From 3d47d486ef4f94c5dab61ec23e5c93377884d805 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Tue, 24 Feb 2026 06:22:03 +0530 Subject: [PATCH] CI: improve more ci worflow --- .github/workflows/alpha.yml | 6 +- .github/workflows/nightly.yml | 18 +- scripts/ci/generate_source_metadata.py | 17 +- scripts/ci/update_source_metadata.py | 283 +++++++++++++------------ scripts/ci/workflow.py | 52 ++++- 5 files changed, 216 insertions(+), 160 deletions(-) diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index 7c25018b..fe59f67d 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -62,7 +62,7 @@ jobs: uses: actions/cache/restore@v3 with: path: | - ~/Library/Alphaer/Xcode/DerivedData + ~/Library/Developer/Xcode/DerivedData ~/Library/Caches/org.swift.swiftpm key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }} @@ -72,7 +72,7 @@ jobs: uses: actions/cache/restore@v3 with: path: | - ~/Library/Alphaer/Xcode/DerivedData + ~/Library/Developer/Xcode/DerivedData ~/Library/Caches/org.swift.swiftpm key: xcode-build-cache-${{ github.ref_name }}- @@ -117,7 +117,7 @@ jobs: uses: actions/cache/save@v3 with: path: | - ~/Library/Alphaer/Xcode/DerivedData + ~/Library/Developer/Xcode/DerivedData ~/Library/Caches/org.swift.swiftpm key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 891cec12..226cae1b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -17,7 +17,6 @@ jobs: env: REF_NAME: nightly CHANNEL: nightly - BUNDLE_ID: com.SideStore.SideStore steps: - uses: actions/checkout@v4 @@ -167,6 +166,8 @@ jobs: # deploy # -------------------------------------------------- - name: Deploy + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PRODUCT_NAME=$(python3 scripts/ci/workflow.py get-product-name) BUNDLE_ID=$(python3 scripts/ci/workflow.py get-bundle-id) @@ -175,7 +176,7 @@ jobs: LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \ "${{ github.workflow }}" "$CHANNEL") - + python3 scripts/ci/workflow.py deploy \ Dependencies/apps-v2.json \ "$SOURCE_JSON" \ @@ -186,4 +187,15 @@ jobs: "$CHANNEL" \ "$BUNDLE_ID" \ "$IPA_NAME" \ - "$LAST_SUCCESSFUL_COMMIT" \ No newline at end of file + "$LAST_SUCCESSFUL_COMMIT" + + python3 scripts/ci/workflow.py upload-release \ + "$RELEASE_NAME" \ + "$RELEASE_TAG" \ + "$IS_BETA" \ + "$VERSION" \ + "$GITHUB_SHA" \ + "$GITHUB_REPOSITORY" \ + "$BUILT_DATE" \ + "$BUILT_DATE_ALT" \ + "$RELEASE_NOTES" diff --git a/scripts/ci/generate_source_metadata.py b/scripts/ci/generate_source_metadata.py index 51c4d4d8..c65d2323 100644 --- a/scripts/ci/generate_source_metadata.py +++ b/scripts/ci/generate_source_metadata.py @@ -5,6 +5,7 @@ import json import subprocess from pathlib import Path import argparse +import textwrap import sys SCRIPT_DIR = Path(__file__).resolve().parent @@ -140,15 +141,15 @@ def main(): formatted = now.strftime("%Y-%m-%dT%H:%M:%SZ") human = now.strftime("%c") - localized_description = f""" -This is release for: - - version: "{args.version}" - - revision: "{args.short_commit}" - - timestamp: "{human}" + localized_description = textwrap.dedent(f""" + This is release for: + - version: "{args.version}" + - revision: "{args.short_commit}" + - timestamp: "{human}" -Release Notes: -{notes} -""".strip() + Release Notes: + {notes} + """).strip() metadata = { "is_beta": bool(args.is_beta), diff --git a/scripts/ci/update_source_metadata.py b/scripts/ci/update_source_metadata.py index 355faed6..b7570143 100755 --- a/scripts/ci/update_source_metadata.py +++ b/scripts/ci/update_source_metadata.py @@ -6,175 +6,176 @@ from pathlib import Path # ---------------------------------------------------------- -# args +# metadata # ---------------------------------------------------------- -if len(sys.argv) < 3: - print("Usage: python3 update_apps.py ") - sys.exit(1) +def load_metadata(metadata_file: Path): + if not metadata_file.exists(): + raise SystemExit(f"Missing metadata file: {metadata_file}") -metadata_file = Path(sys.argv[1]) -source_file = Path(sys.argv[2]) + with open(metadata_file, "r", encoding="utf-8") as f: + meta = json.load(f) + + print(" ====> Required parameter list <====") + for k, v in meta.items(): + print(f"{k}: {v}") + + required = [ + "bundle_identifier", + "version_ipa", + "version_date", + "release_channel", + "size", + "sha256", + "localized_description", + "download_url", + ] + + for r in required: + if not meta.get(r): + raise SystemExit("One or more required metadata fields missing") + + meta["size"] = int(meta["size"]) + meta["release_channel"] = meta["release_channel"].lower() + + return meta # ---------------------------------------------------------- -# load metadata +# source loading # ---------------------------------------------------------- -if not metadata_file.exists(): - print(f"Missing metadata file: {metadata_file}") - sys.exit(1) +def load_source(source_file: Path): + if source_file.exists(): + with open(source_file, "r", encoding="utf-8") as f: + data = json.load(f) + else: + print("source.json missing — creating minimal structure") + data = {"version": 2, "apps": []} -with open(metadata_file, "r", encoding="utf-8") as f: - meta = json.load(f) + if int(data.get("version", 1)) < 2: + raise SystemExit("Only v2 and above are supported") -VERSION_IPA = meta.get("version_ipa") -VERSION_DATE = meta.get("version_date") -IS_BETA = meta.get("is_beta") -RELEASE_CHANNEL = meta.get("release_channel") -SIZE = meta.get("size") -SHA256 = meta.get("sha256") -LOCALIZED_DESCRIPTION = meta.get("localized_description") -DOWNLOAD_URL = meta.get("download_url") -BUNDLE_IDENTIFIER = meta.get("bundle_identifier") - -print(" ====> Required parameter list <====") -print("Bundle Identifier:", BUNDLE_IDENTIFIER) -print("Version:", VERSION_IPA) -print("Version Date:", VERSION_DATE) -print("IsBeta:", IS_BETA) -print("ReleaseChannel:", RELEASE_CHANNEL) -print("Size:", SIZE) -print("Sha256:", SHA256) -print("Localized Description:", LOCALIZED_DESCRIPTION) -print("Download URL:", DOWNLOAD_URL) - - -# ---------------------------------------------------------- -# validation -# ---------------------------------------------------------- - -if ( - not BUNDLE_IDENTIFIER - or not VERSION_IPA - or not VERSION_DATE - or not RELEASE_CHANNEL - or not SIZE - or not SHA256 - or not LOCALIZED_DESCRIPTION - or not DOWNLOAD_URL -): - print("One or more required metadata fields missing") - sys.exit(1) - -SIZE = int(SIZE) -RELEASE_CHANNEL = RELEASE_CHANNEL.lower() - - -# ---------------------------------------------------------- -# load source.json -# ---------------------------------------------------------- - -if source_file.exists(): - with open(source_file, "r", encoding="utf-8") as f: - data = json.load(f) -else: - print("source.json missing — creating minimal structure") - data = { - "version": 2, - "apps": [] - } - -if int(data.get("version", 1)) < 2: - print("Only v2 and above are supported") - sys.exit(1) + return data # ---------------------------------------------------------- # locate app # ---------------------------------------------------------- -apps = data.setdefault("apps", []) +def ensure_app(data, bundle_id): + apps = data.setdefault("apps", []) -app = next( - (a for a in apps if a.get("bundleIdentifier") == BUNDLE_IDENTIFIER), - None -) + app = next( + (a for a in apps if a.get("bundleIdentifier") == bundle_id), + None, + ) -if app is None: - print("App entry missing — creating new app entry") - app = { - "bundleIdentifier": BUNDLE_IDENTIFIER, - "releaseChannels": [] + if app is None: + print("App entry missing — creating new app entry") + app = { + "bundleIdentifier": bundle_id, + "releaseChannels": [], + } + apps.append(app) + + return app + + +# ---------------------------------------------------------- +# update storefront +# ---------------------------------------------------------- + +def update_storefront_if_needed(app, meta): + if meta["release_channel"] == "stable": + app.update({ + "version": meta["version_ipa"], + "versionDate": meta["version_date"], + "size": meta["size"], + "sha256": meta["sha256"], + "localizedDescription": meta["localized_description"], + "downloadURL": meta["download_url"], + }) + + +# ---------------------------------------------------------- +# update release channel (ORIGINAL FORMAT) +# ---------------------------------------------------------- + +def update_release_channel(app, meta): + channels = app.setdefault("releaseChannels", []) + + new_version = { + "version": meta["version_ipa"], + "date": meta["version_date"], + "localizedDescription": meta["localized_description"], + "downloadURL": meta["download_url"], + "size": meta["size"], + "sha256": meta["sha256"], } - apps.append(app) + tracks = [ + t for t in channels + if isinstance(t, dict) + and t.get("track") == meta["release_channel"] + ] -# ---------------------------------------------------------- -# update storefront metadata (stable only) -# ---------------------------------------------------------- + if len(tracks) > 1: + raise SystemExit(f"Multiple tracks named {meta['release_channel']}") -if RELEASE_CHANNEL == "stable": - app.update({ - "version": VERSION_IPA, - "versionDate": VERSION_DATE, - "size": SIZE, - "sha256": SHA256, - "localizedDescription": LOCALIZED_DESCRIPTION, - "downloadURL": DOWNLOAD_URL, - }) - - -# ---------------------------------------------------------- -# releaseChannels update (ORIGINAL FORMAT) -# ---------------------------------------------------------- - -channels = app.setdefault("releaseChannels", []) - -new_version = { - "version": VERSION_IPA, - "date": VERSION_DATE, - "localizedDescription": LOCALIZED_DESCRIPTION, - "downloadURL": DOWNLOAD_URL, - "size": SIZE, - "sha256": SHA256, -} - -# find track -tracks = [ - t for t in channels - if isinstance(t, dict) and t.get("track") == RELEASE_CHANNEL -] - -if len(tracks) > 1: - print(f"Multiple tracks named {RELEASE_CHANNEL}") - sys.exit(1) - -if not tracks: - # create new track at top (original behaviour) - channels.insert(0, { - "track": RELEASE_CHANNEL, - "releases": [new_version], - }) -else: - track = tracks[0] - releases = track.setdefault("releases", []) - - if not releases: - releases.append(new_version) + if not tracks: + channels.insert(0, { + "track": meta["release_channel"], + "releases": [new_version], + }) else: - # replace top entry only (original logic) - releases[0] = new_version + track = tracks[0] + releases = track.setdefault("releases", []) + + if not releases: + releases.append(new_version) + else: + releases[0] = new_version # ---------------------------------------------------------- # save # ---------------------------------------------------------- -print("\nUpdated Sources File:\n") -print(json.dumps(data, indent=2, ensure_ascii=False)) +def save_source(source_file: Path, data): + print("\nUpdated Sources File:\n") + print(json.dumps(data, indent=2, ensure_ascii=False)) -with open(source_file, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) + source_file.parent.mkdir(parents=True, exist_ok=True) -print("JSON successfully updated.") \ No newline at end of file + with open(source_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + print("JSON successfully updated.") + + +# ---------------------------------------------------------- +# main +# ---------------------------------------------------------- + +def main(): + if len(sys.argv) < 3: + print("Usage: python3 update_apps.py ") + sys.exit(1) + + metadata_file = Path(sys.argv[1]) + source_file = Path(sys.argv[2]) + + meta = load_metadata(metadata_file) + data = load_source(source_file) + + app = ensure_app(data, meta["bundle_identifier"]) + + update_storefront_if_needed(app, meta) + update_release_channel(app, meta) + + save_source(source_file, data) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/ci/workflow.py b/scripts/ci/workflow.py index 8dd76378..2339bd48 100644 --- a/scripts/ci/workflow.py +++ b/scripts/ci/workflow.py @@ -6,6 +6,7 @@ import datetime from pathlib import Path import time import json +import textwrap # REPO ROOT relative to script dir @@ -272,7 +273,7 @@ def release_notes(tag): def deploy(repo, source_json, release_tag, short_commit, marketing_version, version, channel, bundle_id, ipa_name, last_successful_commit=None): repo = (ROOT / repo).resolve() ipa_path = ROOT / ipa_name - source_path = repo / source_json + source_json_path = repo / source_json metadata = 'source-metadata.json' if not repo.exists(): @@ -281,7 +282,7 @@ def deploy(repo, source_json, release_tag, short_commit, marketing_version, vers if not ipa_path.exists(): raise SystemExit(f"{ipa_path} missing") - if not source_path.exists(): + if not source_json_path.exists(): raise SystemExit(f"{source_json} missing inside repo") cmd = ( @@ -315,7 +316,7 @@ def deploy(repo, source_json, release_tag, short_commit, marketing_version, vers run("git reset --hard FETCH_HEAD", check=False, cwd=repo) # regenerate after reset so we don't lose changes - run(f"python3 {SCRIPTS}/update_source_metadata.py '{ROOT}/{metadata}' '{source_json}'") + run(f"python3 {SCRIPTS}/update_source_metadata.py '{ROOT}/{metadata}' '{source_json_path}'") run(f"git add --verbose {source_json}", check=False) run(f"git commit -m '{release_tag} - deployed {version}' || true", check=False) @@ -351,6 +352,45 @@ def last_successful_commit(workflow, branch): return None +def upload_release(release_name, release_tag,is_beta,version,commit_sha,repo,built_date,built_date_alt,upstream_recommendation,release_notes): + token = getenv("GH_TOKEN") + if token: + os.environ["GH_TOKEN"] = token + + body = textwrap.dedent(f"""\ + This is an ⚠️ **EXPERIMENTAL** ⚠️ {release_name} build for commit [{commit_sha}](https://github.com/{repo}/commit/{commit_sha}). + + {release_name} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!** + + {upstream_recommendation} + ## Build Info + + Built at (UTC): `{built_date}` + Built at (UTC date): `{built_date_alt}` + Commit SHA: `{commit_sha}` + Version: `{version}` + + {release_notes} + """) + + body_file = ROOT / "release_body.md" + body_file.write_text(body, encoding="utf-8") + + prerelease_flag = "--prerelease" if str(is_beta).lower() in ["true", "1", "yes"] else "" + + run( + f'gh release edit "{release_tag}" ' + f'--title "{release_name}" ' + f'--notes-file "{body_file}" ' + f'{prerelease_flag}' + ) + + run( + f'gh release upload "{release_tag}" ' + f'SideStore.ipa SideStore.dSYMs.zip ' + f'--clobber' + ) + # ---------------------------------------------------------- # ENTRYPOINT # ---------------------------------------------------------- @@ -403,8 +443,10 @@ COMMANDS = { # ---------------------------------------------------------- "last-successful-commit" : (last_successful_commit, 2, " "), "release-notes" : (release_notes, 1, ""), - "deploy" : (deploy, 10, - " [last_successful_commit]"), + "deploy" : (deploy, 10, + " [last_successful_commit]"), + "upload-release" : (upload_release, 9, + " "), } def main():