mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 01:13:19 +08:00
[ci] sync desktop and mobile release downloads to version.json
This commit is contained in:
@@ -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()
|
||||
@@ -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>{base})-(?P<patch>[0-9]+)-(?P<date>[0-9]{{8}})",
|
||||
rf"(?P<base>{base})-(?P<date>[0-9]{{8}})(?:-(?P<patch>[0-9]+))?",
|
||||
rf"(?P<base>{base})(?:-(?P<patch>[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()
|
||||
+21
-46
@@ -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 }}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user