From 13f4116825d59dda1de84fa598e281ea0d765262 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 2 Oct 2026 03:14:10 +0800 Subject: [PATCH] [ci] sync desktop and mobile release downloads to version.json --- .../scripts/test_update_version_manifest.py | 114 ++++++++++ .github/scripts/update_version_manifest.py | 130 ++++++++++++ .github/workflows/build.yml | 67 ++---- .github/workflows/release-mobile.yml | 37 +++- .github/workflows/update-version-json.yml | 196 ++++++------------ apps/android/README.md | 4 +- apps/ios/README.md | 16 +- 7 files changed, 374 insertions(+), 190 deletions(-) create mode 100644 .github/scripts/test_update_version_manifest.py create mode 100644 .github/scripts/update_version_manifest.py diff --git a/.github/scripts/test_update_version_manifest.py b/.github/scripts/test_update_version_manifest.py new file mode 100644 index 00000000..fc65dd03 --- /dev/null +++ b/.github/scripts/test_update_version_manifest.py @@ -0,0 +1,114 @@ +"""Regression coverage for independent and combined release metadata.""" + +import copy +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import update_version_manifest as manifest + + +class ManifestTest(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="crossdesk-manifest-") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.artifacts = self.root / "release" + self.artifacts.mkdir() + self.current = { + "version": "1.6.0-20260930", "latest_version": "1.6.0-20260930", + "releaseDate": "2026-09-30", "releaseNotes": "Desktop notes", "patch": 0, + "downloads": {"windows-x64": {"url": "https://downloads.crossdesk.cn/existing.exe"}}, + "custom": {"keep": True}, + } + + def files(self, *names): + for path in self.artifacts.iterdir(): + path.unlink() + for name in names: + (self.artifacts / name).write_bytes(b"release fixture") + return manifest.artifact_downloads(self.artifacts) + + def merge(self, tag, downloads, current=None): + return manifest.merge_release(current or self.current, tag, tag, "Release notes", downloads) + + def test_mobile_preserves_desktop_metadata_and_downloads(self): + downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk") + original = copy.deepcopy(self.current) + result = self.merge("android-v1.6.0-20261002", downloads) + for key, value in original.items(): + if key != "downloads": + self.assertEqual(result[key], value) + self.assertEqual(result["downloads"]["windows-x64"], original["downloads"]["windows-x64"]) + self.assertEqual(result["downloads"]["android-arm64"]["url"], + "https://downloads.crossdesk.cn/crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk") + self.assertEqual(result["platforms"]["android"]["latest_version"], "1.6.0-20261002") + self.assertEqual(self.current, original) + + def test_sequential_platform_releases_retain_each_other(self): + for order in (("ios", "android"), ("android", "ios")): + result = copy.deepcopy(self.current) + for platform in order: + version, extension = ("0.0.2", "zip") if platform == "ios" else ("1.6.0", "apk") + downloads = self.files(f"crossdesk-{platform}-arm64-unsigned-v{version}-20261002.{extension}") + result = self.merge(f"{platform}-v{version}-20261002", downloads, result) + self.assertEqual(set(result["platforms"]), {"ios", "android"}) + self.assertEqual(result["version"], self.current["version"]) + self.assertTrue({"windows-x64", "ios-arm64", "android-arm64"} <= result["downloads"].keys()) + + def test_combined_release_records_independent_mobile_versions(self): + downloads = self.files("crossdesk-win-x64-v1.6.1-20261002.exe", + "crossdesk-win-x64-portable-v1.6.1-20261002.zip", + "crossdesk-ios-arm64-unsigned-v0.0.2-20261002.zip", + "crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk") + result = self.merge("v1.6.1-20261002", downloads) + self.assertEqual(result["version"], "1.6.1-20261002") + self.assertEqual(result["platforms"]["ios"]["version"], "0.0.2-20261002") + self.assertEqual(result["platforms"]["android"]["version"], "1.6.0-20261002") + + def test_mobile_tag_must_match_artifact_and_platform(self): + downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk") + for tag in ("android-v1.7.0-20261002", "ios-v1.6.0-20261002"): + with self.assertRaises(ValueError): + self.merge(tag, downloads) + + def test_invalid_current_manifest_is_rejected(self): + downloads = self.files("crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk") + for current in ([], {}, {"downloads": []}, {"downloads": {}, "platforms": []}): + with self.assertRaises(ValueError): + manifest.merge_release(current, "android-v1.6.0-20261002", "Android", "", downloads) + + def test_missing_duplicate_and_invalid_artifacts_are_rejected(self): + for names in ((), ("crossdesk-android-arm64-unsigned-v1.6.0-20260230.apk",), + ("crossdesk-android-arm64-unsigned-v1.6.0-20261001.apk", + "crossdesk-android-arm64-unsigned-v1.6.0-20261002.apk")): + with self.assertRaises(ValueError): + self.files(*names) + + def test_hotfix_version_and_date_formats(self): + for value in ("v1.6.0-2-20261002", "v1.6.0-20261002-2"): + info = manifest.version_info(value) + self.assertEqual(info["patch"], 2) + self.assertEqual(info["releaseDate"], "2026-10-02") + + def test_cli_preserves_literal_notes_and_release_metadata(self): + self.files("crossdesk-ios-arm64-unsigned-v0.0.2-20261002.zip") + notes = '中文 release notes\n"quoted" $(not-a-command) $HOME `literal`' + release = {"tagName": "ios-v0.0.2-20261002", "name": "iOS release", "body": notes} + (self.root / "current.json").write_text(json.dumps(self.current)) + (self.root / "release.json").write_text(json.dumps(release)) + output = self.root / "version.json" + subprocess.run([sys.executable, str(Path(manifest.__file__)), + "--current", str(self.root / "current.json"), "--output", str(output), + "--artifacts", str(self.artifacts), "--release-json", str(self.root / "release.json")], + check=True, capture_output=True, text=True) + result = json.loads(output.read_text()) + self.assertEqual(result["platforms"]["ios"]["releaseNotes"], notes) + self.assertEqual(result["releaseNotes"], "Desktop notes") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/update_version_manifest.py b/.github/scripts/update_version_manifest.py new file mode 100644 index 00000000..5450e7cb --- /dev/null +++ b/.github/scripts/update_version_manifest.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Merge a release's server downloads into the shared version.json.""" + +import argparse +import datetime +import json +from pathlib import Path +import re + + +DOWNLOAD_BASE = "https://downloads.crossdesk.cn/" +ARTIFACTS = { + "windows-x64": ("win-x64", "exe"), + "windows-x64-portable": ("win-x64-portable", "zip"), + "macos-x64": ("macos-x64", "pkg"), + "macos-arm64": ("macos-arm64", "pkg"), + "linux-amd64": ("linux-amd64", "deb"), + "linux-arm64": ("linux-arm64", "deb"), + "ios-arm64": ("ios-arm64-unsigned", "zip"), + "android-arm64": ("android-arm64-unsigned", "apk"), +} + + +def version_info(version, fallback_date=None): + version = version.removeprefix("v") + base = r"[0-9]+(?:\.[0-9]+){1,3}" + for pattern in ( + rf"(?P{base})-(?P[0-9]+)-(?P[0-9]{{8}})", + rf"(?P{base})-(?P[0-9]{{8}})(?:-(?P[0-9]+))?", + rf"(?P{base})(?:-(?P[0-9]+))?", + ): + match = re.fullmatch(pattern, version) + if match: + fields = match.groupdict() + date = fields.get("date") + release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat() + if date else fallback_date) + if not release_date: + raise ValueError(f"Release date is missing for {version}") + datetime.date.fromisoformat(release_date) + return {"latest_version": version, "version": version, + "releaseDate": release_date, "patch": int(fields.get("patch") or 0)} + raise ValueError(f"Invalid release version: {version}") + + +def artifact_downloads(directory): + downloads = {} + for path in sorted(Path(directory).iterdir()): + if not path.is_file(): + continue + for key, (prefix, extension) in ARTIFACTS.items(): + match = re.fullmatch(rf"crossdesk-{prefix}-v([0-9][0-9.\-]*)\.{extension}", path.name) + if not match: + continue + if key in downloads: + raise ValueError(f"Multiple release artifacts for {key}") + metadata = version_info(match[1]) + downloads[key] = {"url": DOWNLOAD_BASE + path.name, "filename": path.name, + "version": metadata["version"], "releaseDate": metadata["releaseDate"]} + break + if not downloads: + raise ValueError("No supported release artifacts found") + return downloads + + +def merge_release(current, tag, name, notes, downloads, published_date=None): + if not isinstance(current, dict) or not isinstance(current.get("downloads"), dict): + raise ValueError("Current version.json must contain a downloads object") + if not isinstance(current.get("platforms", {}), dict): + raise ValueError("Current version.json platforms must be an object") + mobile = re.fullmatch(r"(ios|android)-(v.+)", tag) + if mobile: + platform, version = mobile.groups() + expected_key = f"{platform}-arm64" + if set(downloads) != {expected_key}: + raise ValueError(f"{tag} must contain only the {expected_key} release artifact") + metadata = version_info(version, published_date) + if downloads[expected_key]["version"] != metadata["version"]: + raise ValueError("Mobile source tag does not match the artifact version") + else: + if not tag.startswith("v"): + raise ValueError(f"Expected a version tag, got {tag!r}") + metadata = version_info(tag, published_date) + + result = dict(current) + result["downloads"] = {**current["downloads"], **downloads} + result["platforms"] = dict(current.get("platforms", {})) + release = {**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag} + if not mobile: + result.update(release) + for platform in ("ios", "android"): + download = downloads.get(f"{platform}-arm64") + if download: + info = version_info(download["version"]) + result["platforms"][platform] = { + **info, "releaseName": name, "releaseNotes": notes, "tagName": tag, + } + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--current", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--artifacts", required=True, type=Path) + parser.add_argument("--tag") + parser.add_argument("--name") + parser.add_argument("--notes", type=Path) + parser.add_argument("--release-json", type=Path) + args = parser.parse_args() + if args.release_json: + release = json.loads(args.release_json.read_text()) + tag = release["tagName"] + name = release.get("name") or tag + notes = release.get("body") or "" + published_date = (release.get("publishedAt") or "").split("T")[0] or None + else: + if not args.tag or not args.name or not args.notes: + parser.error("Supply --release-json or --tag, --name and --notes") + tag, name, notes = args.tag, args.name, args.notes.read_text() + published_date = None + current = json.loads(args.current.read_text()) + downloads = artifact_downloads(args.artifacts) + result = merge_release(current, tag, name, notes, downloads, published_date) + args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n") + print(f"Updated {tag}: {', '.join(downloads)}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ad88556..d679951c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -561,6 +561,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') needs: [build-linux, build-macos, build-ios, build-android, build-windows-x64] runs-on: ubuntu-latest + concurrency: + group: publish-release-metadata + cancel-in-progress: false steps: - name: Checkout repository @@ -571,6 +574,9 @@ jobs: with: path: artifacts + - name: Verify release manifest updates + run: python3 -B -m unittest discover -s .github/scripts -p 'test_*.py' -v + - name: Extract version number id: version shell: bash @@ -681,63 +687,32 @@ jobs: - name: Upload artifacts to server uses: burnett01/rsync-deployments@5.2 with: - switches: -avzr --progress --delete + switches: -avzr --progress path: release/* remote_path: /var/www/html/downloads/ remote_host: ${{ secrets.SERVER_HOST }} remote_user: ${{ secrets.SERVER_USER }} remote_key: ${{ secrets.SERVER_KEY }} - - name: Generate version.json + - name: Download current version.json run: | - cat > version.json << EOF - { - "latest_version": "${{ steps.version.outputs.VERSION_NUM }}", - "version": "${{ steps.version.outputs.VERSION_NUM }}", - "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", - "patch": ${{ steps.version.outputs.PATCH_NUMBER }}, - "releaseName": "", - "releaseNotes": "", - "tagName": "${{ steps.version.outputs.VERSION_WITH_V }}", - "downloads": { - "windows-x64": { - "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe", - "filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe" - }, - "windows-x64-portable": { - "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip", - "filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip" - }, - "macos-x64": { - "url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg", - "filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg" - }, - "macos-arm64": { - "url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg", - "filename": "crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg" - }, - "linux-amd64": { - "url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb", - "filename": "crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb" - }, - "linux-arm64": { - "url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb", - "filename": "crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb" - } - } - } - EOF - jq --arg release_name "Release ${{ steps.version.outputs.VERSION_WITH_V }}" \ - --rawfile release_notes "$RUNNER_TEMP/release-notes.md" \ - '.releaseName = $release_name | .releaseNotes = $release_notes' \ - version.json > version-with-notes.json - mv version-with-notes.json version.json - cat version.json + curl --fail --silent --show-error --retry 3 --max-time 30 \ + -H 'Cache-Control: no-cache' \ + https://version.crossdesk.cn/version.json -o current-version.json + + - name: Generate version.json for all platforms + env: + RELEASE_TAG: ${{ steps.version.outputs.VERSION_WITH_V }} + run: | + python3 .github/scripts/update_version_manifest.py \ + --current current-version.json --output version.json --artifacts release \ + --tag "${RELEASE_TAG}" --name "Release ${RELEASE_TAG}" \ + --notes "${RUNNER_TEMP}/release-notes.md" - name: Upload version.json to server uses: burnett01/rsync-deployments@5.2 with: - switches: -avzr --delete + switches: -avzr path: version.json remote_path: /var/www/html/version/ remote_host: ${{ secrets.SERVER_HOST }} diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 58dd0f46..3c2cb395 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -76,7 +76,7 @@ jobs: permissions: contents: write concurrency: - group: mobile-release-${{ needs.prepare.outputs.tag }} + group: publish-release-metadata cancel-in-progress: false env: PLATFORM: ${{ needs.prepare.outputs.platform }} @@ -85,6 +85,12 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} BUILD_VERSION: ${{ needs.build-ios.outputs.version || needs.build-android.outputs.version }} steps: + - name: Checkout publishing scripts + uses: actions/checkout@v5 + + - name: Verify release manifest updates + run: python3 -B -m unittest discover -s .github/scripts -p 'test_*.py' -v + - name: Verify release version shell: bash run: | @@ -143,7 +149,34 @@ jobs: remote_user: ${{ secrets.SERVER_USER }} remote_key: ${{ secrets.SERVER_KEY }} + - name: Download current version.json + shell: bash + run: | + curl --fail --silent --show-error --retry 3 --max-time 30 \ + -H 'Cache-Control: no-cache' \ + https://version.crossdesk.cn/version.json -o current-version.json + + - name: Update mobile version and download URL + shell: bash + env: + RELEASE_NAME: ${{ env.PLATFORM == 'ios' && 'iOS' || 'Android' }} ${{ env.VERSION }} + run: | + python3 .github/scripts/update_version_manifest.py \ + --current current-version.json --output version.json --artifacts release \ + --tag "${SOURCE_TAG}" --name "${RELEASE_NAME}" \ + --notes "${RUNNER_TEMP}/mobile-release-notes.md" + + - name: Upload version.json to server + uses: burnett01/rsync-deployments@5.2 + with: + switches: -avzr + path: version.json + remote_path: /var/www/html/version/ + remote_host: ${{ secrets.SERVER_HOST }} + remote_user: ${{ secrets.SERVER_USER }} + remote_key: ${{ secrets.SERVER_KEY }} + - name: Summarize release shell: bash run: | - echo "Published [${SOURCE_TAG}](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${SOURCE_TAG}) and uploaded its unsigned ${PLATFORM} artifact to the download server." >> "${GITHUB_STEP_SUMMARY}" + echo "Published [${SOURCE_TAG}](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${SOURCE_TAG}), uploaded its unsigned ${PLATFORM} artifact to the download server, and updated version.json." >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/update-version-json.yml b/.github/workflows/update-version-json.yml index c557b5a7..bac5809b 100644 --- a/.github/workflows/update-version-json.yml +++ b/.github/workflows/update-version-json.yml @@ -3,161 +3,85 @@ name: Update version.json from Release on: release: types: [published, edited] + workflow_dispatch: + inputs: + source_tag: + description: "Existing version tag whose assets and metadata should be uploaded to the server." + required: true + type: string permissions: - contents: write + contents: read jobs: update-version-json: - name: Update version.json with release information - if: ${{ !startsWith(github.event.release.tag_name, 'ios-v') && !startsWith(github.event.release.tag_name, 'android-v') }} - runs-on: ubuntu-latest - + name: Upload release downloads and update version.json + if: github.event_name == 'workflow_dispatch' || (github.event.release.tag_name != 'latest' && !github.event.release.prerelease) + runs-on: ubuntu-24.04 + concurrency: + group: publish-release-metadata + cancel-in-progress: false + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.source_tag || github.event.release.tag_name }} steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout publishing scripts + uses: actions/checkout@v5 with: - token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.repository.default_branch }} - - name: Extract version from tag - id: version + - name: Verify release manifest updates + run: python3 -B -m unittest discover -s .github/scripts -p 'test_*.py' -v + + - name: Download release information and assets shell: bash run: | - TAG_NAME="${{ github.event.release.tag_name }}" - TAG_VERSION="${TAG_NAME#v}" - VERSION_FULL="${TAG_VERSION}" - VERSION_BASE="${TAG_VERSION}" - PATCH_NUMBER=0 + python3 - <<'PY' + import os + import re + if not re.fullmatch(r'(?:ios-|android-)?v[0-9]+(?:\.[0-9]+){1,3}(?:-[0-9]+)*', os.environ['RELEASE_TAG']): + raise ValueError('Use a desktop, iOS or Android version tag') + PY + gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ + --json tagName,name,body,publishedAt,isDraft,isPrerelease > release.json + python3 - <<'PY' + import json + from pathlib import Path + release = json.loads(Path('release.json').read_text()) + if release['isDraft'] or release['isPrerelease']: + raise ValueError('Only published stable releases update version.json') + PY + gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ + --pattern 'crossdesk-*' --dir release - # Extract date and patch from tags such as v1.2.3-1-20251113. - if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then - VERSION_BASE="${BASH_REMATCH[1]}" - PATCH_NUMBER="${BASH_REMATCH[3]}" - DATE_STR="${BASH_REMATCH[4]}" - BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" - elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then - VERSION_BASE="${BASH_REMATCH[1]}" - DATE_STR="${BASH_REMATCH[3]}" - PATCH_NUMBER="${BASH_REMATCH[4]}" - BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" - elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then - VERSION_BASE="${BASH_REMATCH[1]}" - DATE_STR="${BASH_REMATCH[3]}" - BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" - else - # Use release published date - BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1) - fi + - name: Upload release assets to download server + uses: burnett01/rsync-deployments@5.2 + with: + switches: -avzr --progress + path: release/* + remote_path: /var/www/html/downloads/ + remote_host: ${{ secrets.SERVER_HOST }} + remote_user: ${{ secrets.SERVER_USER }} + remote_key: ${{ secrets.SERVER_KEY }} - echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT - echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT - echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT - echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT - echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT - - - name: Install jq - run: sudo apt-get update && sudo apt-get install -y jq - - - name: Get release information - id: release_info + - name: Download current version.json + shell: bash run: | - # Use jq to properly escape JSON - RELEASE_BODY="${{ github.event.release.body }}" - RELEASE_NAME="${{ github.event.release.name }}" + curl --fail --silent --show-error --retry 3 --max-time 30 \ + -H 'Cache-Control: no-cache' \ + https://version.crossdesk.cn/version.json -o current-version.json - # Handle empty values - if [ -z "$RELEASE_BODY" ]; then - RELEASE_BODY="" - fi - if [ -z "$RELEASE_NAME" ]; then - RELEASE_NAME="" - fi - - # Save to temporary files for proper handling - echo -n "$RELEASE_BODY" > /tmp/release_body.txt - echo -n "$RELEASE_NAME" > /tmp/release_name.txt - - # Use jq to escape JSON strings - RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt) - RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt) - - echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT - echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT - - - name: Download current version.json from server - id: download_version - continue-on-error: true + - name: Merge release version and download URLs + shell: bash run: | - # Try to download current version.json from server - curl -f -s "https://version.crossdesk.cn/version.json" -o version.json || echo "Failed to download, will create new one" - - - name: Generate or update version.json - run: | - # If version.json exists, try to preserve downloads section - if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then - EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json) - if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then - DOWNLOADS_JSON="$EXISTING_DOWNLOADS" - else - DOWNLOADS_JSON="" - fi - else - DOWNLOADS_JSON="" - fi - - # If downloads is empty, use default structure - if [ -z "$DOWNLOADS_JSON" ]; then - DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF - { - "windows-x64": { - "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe", - "filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe" - }, - "windows-x64-portable": { - "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip", - "filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip" - }, - "macos-x64": { - "url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg", - "filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg" - }, - "macos-arm64": { - "url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg", - "filename": "crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg" - }, - "linux-amd64": { - "url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb", - "filename": "crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb" - }, - "linux-arm64": { - "url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb", - "filename": "crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb" - } - } - DOWNLOADS_EOF - ) - fi - - # Generate version.json using cat and heredoc - cat > version.json << EOF - { - "latest_version": "${{ steps.version.outputs.VERSION_FULL }}", - "version": "${{ steps.version.outputs.VERSION_FULL }}", - "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", - "patch": ${{ steps.version.outputs.PATCH_NUMBER }}, - "releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }}, - "releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }}, - "tagName": "${{ steps.version.outputs.TAG_NAME }}", - "downloads": ${DOWNLOADS_JSON} - } - EOF - - cat version.json + python3 .github/scripts/update_version_manifest.py \ + --current current-version.json --output version.json --artifacts release \ + --release-json release.json - name: Upload version.json to server uses: burnett01/rsync-deployments@5.2 with: - switches: -avzr --delete + switches: -avzr path: version.json remote_path: /var/www/html/version/ remote_host: ${{ secrets.SERVER_HOST }} diff --git a/apps/android/README.md b/apps/android/README.md index 14ee8439..cbae5fe5 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -84,7 +84,9 @@ git push origin android-v1.6.0-20261002 该标签只触发安卓构建,将未签名 Release APK 发布到独立的 GitHub Release 和现有下载服务器。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。 -移动端独立发布不会更新桌面 `latest` 标签、GitHub 的最新 Release 选择或桌面 `version.json`。下载服务器上传保留其他平台文件,复用仓库已有的 `SERVER_HOST`、`SERVER_USER`、`SERVER_KEY` Secrets。普通桌面版本标签仍通过 `Build and Release` 发布全部平台;单独运行 `Build Android` 仍只上传 Actions 构建产物。 +移动端独立发布保留桌面 `latest` 标签和 GitHub 的最新 Release 选择。APK 上传完成后,将服务器下载地址合并到共享 `https://version.crossdesk.cn/version.json` 的 `downloads.android-arm64`,将安卓版本、发布日期和说明写入 `platforms.android`;保留顶层桌面版本信息和其他平台的下载项。下载服务器上传保留其他平台文件,复用仓库已有的 `SERVER_HOST`、`SERVER_USER`、`SERVER_KEY` Secrets。普通桌面版本标签仍通过 `Build and Release` 发布全部平台并更新各平台下载项;所有发布任务更新版本文件时使用同一队列。单独运行 `Build Android` 仍只上传 Actions 构建产物。 + +已有 Release 需要补传服务器或刷新下载信息时,可手动运行 `Update version.json from Release` 并填写已有 `source_tag`,无需重新构建。 工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c 和 Xmake 3.1.1。Gradle 使用工程内的 Wrapper,原生构建器固定 Xmake 包仓库版本。Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。 diff --git a/apps/ios/README.md b/apps/ios/README.md index eb864f57..b6222f86 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -112,11 +112,17 @@ manually and enter the existing tag in `source_tag`; it builds that tag's source even when the workflow is dispatched from a different branch. Source metadata records the iOS tag and exact application/MiniRTC commits. -Mobile releases keep the desktop `latest` tag, GitHub's latest release selection -and desktop `version.json` unchanged. Uploads preserve other platforms' files -on the download server and use the existing `SERVER_HOST`, `SERVER_USER` and -`SERVER_KEY` repository secrets. Regular desktop version tags still publish all -platforms through `Build and Release`. +Mobile releases keep the desktop `latest` tag and GitHub's latest release +selection unchanged. After uploading the ZIP, the workflow merges its server +URL into `downloads.ios-arm64` and its version, date and release notes into +`platforms.ios` in the shared `https://version.crossdesk.cn/version.json`. +Top-level desktop version information and other platforms' entries are retained. +Uploads preserve other platforms' files and use the existing `SERVER_HOST`, +`SERVER_USER` and `SERVER_KEY` repository secrets. Regular desktop version tags +still publish all platforms and update their download entries through +`Build and Release`. All publishing jobs share one queue when updating the manifest. +To upload an existing release and refresh its download metadata without rebuilding, +run `Update version.json from Release` with its existing `source_tag`. No Apple signing secrets are needed. Sign the app yourself before installing it on a device. Push any referenced MiniRTC commit before the parent repository