Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/cleanup-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Cleanup Old Artifacts

on:
schedule:
- cron: '0 2 * * 0'
workflow_dispatch:

jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Cleanup old artifacts
uses: actions/github-script@v7
with:
script: |
const artifacts = await github.rest.actions.listArtifactsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

const sortedArtifacts = artifacts.data.artifacts.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);

const toDelete = sortedArtifacts.slice(5);

for (const artifact of toDelete) {
console.log(`Deleting artifact: ${artifact.name} (${artifact.created_at})`);
await github.rest.actions.deleteArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact.id,
});
}

console.log(`Cleaned up ${toDelete.length} old artifacts`);
211 changes: 211 additions & 0 deletions scripts/remove_releases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""Utility script to purge prerelease GitHub releases for CollapseLoader."""

import argparse
import logging
import os
import sys
from typing import Any, Iterable, List, Sequence

import requests
from dotenv import load_dotenv

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger("remove_releases")

load_dotenv()

GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
REPO_OWNER = "dest4590"
REPO_NAME = "CollapseLoader"
PREFIX = "prerelease"
ALL_RELEASES_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/releases"


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Delete GitHub releases whose tag names start with the prerelease prefix."
)
)
parser.add_argument(
"-a",
"--all",
action="store_true",
help="Delete all prerelease releases (including the latest). Without this flag, the newest prerelease is kept.",
)
parser.add_argument(
"--prefix",
default=PREFIX,
help="Tag prefix to target (default: %(default)s)",
)
return parser.parse_args(argv)


def build_headers(token: str) -> dict[str, str]:
return {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json",
}


def fetch_all_releases(headers: dict[str, str]) -> List[dict[str, Any]]:
try:
logger.info("Fetching all releases from %s", ALL_RELEASES_URL)
response = requests.get(ALL_RELEASES_URL, headers=headers, timeout=30)
response.raise_for_status()
releases = response.json()
logger.info("Number of releases fetched: %s", len(releases))
return releases
except requests.RequestException as exc:
logger.error("Error fetching releases: %s", exc)
return []


def describe_release(release: dict[str, Any]) -> str:
tag = release.get("tag_name", "<unknown>")
name = release.get("name") or "(no title)"
created = release.get("created_at") or "unknown timestamp"
return f"tag={tag} name={name!r} created_at={created}"


def filter_prereleases(
releases: Iterable[dict[str, Any]], prefix: str
) -> List[dict[str, Any]]:
pref = prefix.lower()
filtered: List[dict[str, Any]] = []
for release in releases:
tag = (release.get("tag_name") or "").lower()
if not tag:
continue
if release.get("draft"):
logger.debug("Skipping draft release %s", describe_release(release))
continue
if tag.startswith(pref):
filtered.append(release)
return filtered


def delete_release(release: dict[str, Any], headers: dict[str, str]) -> bool:
tag_name = release.get("tag_name")
delete_url = release.get("url")
if not delete_url or not tag_name:
logger.error("Release payload missing url or tag_name: %s", release)
return False

try:
logger.info("Deleting release: %s", describe_release(release))
response = requests.delete(delete_url, headers=headers, timeout=30)
if response.status_code == 204:
logger.info("Successfully deleted release %s", tag_name)
return True
logger.error(
"Failed to delete release %s: Status Code %s",
tag_name,
response.status_code,
)
except requests.RequestException as exc:
logger.error("Error deleting release %s: %s", tag_name, exc)
return False


def delete_tag(tag_name: str, headers: dict[str, str]) -> bool:
delete_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/git/refs/tags/{tag_name}"
try:
logger.info("Deleting tag: %s", tag_name)
response = requests.delete(delete_url, headers=headers, timeout=30)
if response.status_code == 204:
logger.info("Successfully deleted tag %s", tag_name)
return True
logger.error(
"Failed to delete tag %s: Status Code %s", tag_name, response.status_code
)
except requests.RequestException as exc:
logger.error("Error deleting tag %s: %s", tag_name, exc)
return False


def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)

token = GITHUB_TOKEN
if not token:
logger.error(
"GITHUB_TOKEN is not set. Please provide a valid token in the environment."
)
return 1

headers = build_headers(token)

releases = fetch_all_releases(headers)
if not releases:
logger.warning("No releases retrieved. Exiting.")
return 1

prerelease_candidates = filter_prereleases(releases, args.prefix)

if not prerelease_candidates:
logger.info(
"No prerelease releases with prefix '%s' found. Nothing to delete.",
args.prefix,
)
return 0

logger.info(
"Found %s prerelease release(s) matching prefix '%s'.",
len(prerelease_candidates),
args.prefix,
)

to_delete = prerelease_candidates
if not args.all:
latest = prerelease_candidates[0]
logger.info(
"Keeping most recent prerelease: %s (use --all to delete it as well)",
describe_release(latest),
)
to_delete = prerelease_candidates[1:]

if not to_delete:
logger.info("No releases left to delete after applying filters.")
return 0

deleted_releases = 0
deleted_tags = 0

for release in to_delete:
tag_name = release.get("tag_name")
if not tag_name:
logger.warning("Skipping release without tag name: %s", release)
continue

if delete_release(release, headers):
deleted_releases += 1
if delete_tag(tag_name, headers):
deleted_tags += 1
else:
logger.warning(
"Release %s deleted but failed to remove tag. Manual cleanup may be required.",
tag_name,
)
else:
logger.warning(
"Skipping tag deletion for %s because release removal did not succeed.",
tag_name,
)

logger.info(
"Deletion complete. Releases removed: %s, tags removed: %s",
deleted_releases,
deleted_tags,
)

return 0 if deleted_releases else 1


if __name__ == "__main__":
sys.exit(main())
1 change: 0 additions & 1 deletion src-tauri/src/commands/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ fn get_client_by_id(id: u32) -> Result<Client, String> {

#[tauri::command]
pub fn get_app_logs() -> Vec<String> {
log_debug!("Retrieving application logs");
logging::APP_LOGS
.lock()
.map(|logs| logs.clone())
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/core/clients/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ impl Client {
let app_handle_clone_for_run = options.app_handle.clone();
let app_handle_clone_for_crash_handling = options.app_handle.clone();
let optional_analytics = SETTINGS.lock().is_ok_and(|s| s.optional_telemetry.value);
let cordshare = SETTINGS.lock().is_ok_and(|s| s.cordshare.value);
// let cordshare = SETTINGS.lock().is_ok_and(|s| s.cordshare.value);
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the commented cordshare variable declaration entirely rather than leaving it as a comment to maintain clean code.

Suggested change
// let cordshare = SETTINGS.lock().is_ok_and(|s| s.cordshare.value);

Copilot uses AI. Check for mistakes.
let irc_chat = SETTINGS.lock().is_ok_and(|s| s.irc_chat.value);

let agent_arguments = AgentArguments::new(
Expand All @@ -595,7 +595,7 @@ impl Client {
} else {
optional_analytics
},
cordshare,
// cordshare,
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented cordshare argument should be completely removed from the AgentArguments::new call rather than commented out.

Suggested change
// cordshare,

Copilot uses AI. Check for mistakes.
irc_chat,
);

Expand Down
10 changes: 5 additions & 5 deletions src-tauri/src/core/clients/internal/agent_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct AgentArguments {
token: String,
client_name: String,
analytics: bool,
cordshare: bool,
// cordshare: bool,
ircchat: bool,
Comment on lines 17 to 21
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of commenting out the cordshare field, it should be completely removed from the struct definition to avoid dead code. The commented code can cause confusion and should be cleaned up.

Copilot uses AI. Check for mistakes.
}

Expand All @@ -26,14 +26,14 @@ impl AgentArguments {
token: String,
client_name: String,
analytics: bool,
cordshare: bool,
// cordshare: bool,
ircchat: bool,
Comment on lines 26 to 30
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented cordshare parameter should be removed from the constructor function signature rather than commented out to maintain clean code.

Copilot uses AI. Check for mistakes.
) -> Self {
Self {
token,
client_name,
analytics,
cordshare,
// cordshare,
ircchat,
}
Comment on lines 32 to 38
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the commented cordshare field assignment from the struct initialization to eliminate dead code.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -47,11 +47,11 @@ impl AgentArguments {

pub fn log_info(&self) {
log_info!(
"Running client with this agent arguments: Token: {}, Client Name: {}, Analytics: {}, Cordshare: {}, IRC Chat: {}",
"Running client with this agent arguments: Token: {}, Client Name: {}, Analytics: {}, IRC Chat: {}",
"*".repeat(self.token.len() / 2),
self.client_name,
self.analytics,
self.cordshare,
// self.cordshare,
self.ircchat
);
}
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/core/storage/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ define_settings! {
language: Setting<String> = ("en".to_string(), true),
discord_rpc_enabled: Setting<bool> = (true, true),
optional_telemetry: Setting<bool> = (true, true),
cordshare: Setting<bool> = (true, true),
// cordshare: Setting<bool> = (true, true),
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of commenting out the cordshare setting, it should be completely removed from the settings definition to avoid confusion and maintain clean code.

Suggested change
// cordshare: Setting<bool> = (true, true),

Copilot uses AI. Check for mistakes.
irc_chat: Setting<bool> = (true, true),
hash_verify: Setting<bool> = (true, true),
sync_client_settings: Setting<bool> = (true, true),
Expand Down
Loading