mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
2b2f2cfad9
- Move the R2 sync out of release.yml into a standalone sync-r2.yml triggered on release promotion (release: released) or manual dispatch, so the mirror follows the same gate as GitHub's /releases/latest - Hard-fail on the official repo when R2 secrets are missing (a silently stale mirror strands updater users); forks may still skip - Only the tag that is currently releases/latest may rewrite the root manifests or prune old versions; backfills of older tags restore versioned files only - Resolve releases/latest with retries and fail instead of guessing on API errors; re-verify right before publishing the root manifests - Add scripts/rewrite-updater-manifest.mjs to point latest.json download URLs at the mirror (minisign signatures cover file contents and stay valid); upload the macOS .tar.gz updater payload alongside installers - Put https://dl.ccswitch.io/latest.json first in the updater endpoints with GitHub as connectivity fallback
223 lines
9.9 KiB
YAML
223 lines
9.9 KiB
YAML
# Mirror release assets to Cloudflare R2 so ccswitch.io/download can serve
|
|
# them directly (dl.ccswitch.io), and mirror the Tauri updater manifest and
|
|
# artifacts so in-app updates avoid GitHub too (the updater verifies minisign
|
|
# signatures client-side, so the mirror is untrusted).
|
|
#
|
|
# Triggered on release promotion (`released` fires when a prerelease is
|
|
# flipped to a full release, or a release is published as stable directly) —
|
|
# NOT on tag push. This keeps the R2 root manifests behind the same gate as
|
|
# GitHub's /releases/latest/: while a release sits as prerelease, neither the
|
|
# download page nor the updater sees it. Additionally, a run only touches the
|
|
# root manifests (and prunes old versions) when its tag IS releases/latest —
|
|
# manual backfills of older tags restore versioned files only.
|
|
#
|
|
# Caveat (by design of tauri-plugin-updater): the updater stops at the first
|
|
# endpoint whose response parses, so a stale-but-valid R2 manifest would mask
|
|
# newer GitHub releases. That is why this sync hard-fails on the official
|
|
# repo when the R2 secrets are missing, instead of silently skipping.
|
|
#
|
|
# Required secrets: R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY
|
|
name: Sync release to R2
|
|
|
|
on:
|
|
release:
|
|
types: [released]
|
|
# Manual recovery/backfill: re-sync any existing release tag.
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Release tag to sync (e.g. v3.18.0)'
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# latest.json / manifest.json are last-writer-wins — serialize runs and keep
|
|
# every trigger queued instead of the default replace-the-pending behavior.
|
|
concurrency:
|
|
group: sync-r2
|
|
cancel-in-progress: false
|
|
queue: max
|
|
|
|
jobs:
|
|
sync-to-r2:
|
|
name: Sync release to R2
|
|
runs-on: ubuntu-22.04
|
|
env:
|
|
R2_BUCKET: cc-switch-releases
|
|
R2_PUBLIC_BASE_URL: https://dl.ccswitch.io
|
|
KEEP_VERSIONS: "5"
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
AWS_DEFAULT_REGION: auto
|
|
# aws-cli >= 2.23 defaults to CRC checksums that R2 rejects.
|
|
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
|
|
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
|
|
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
|
steps:
|
|
- name: Check R2 secrets
|
|
id: r2
|
|
env:
|
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
run: |
|
|
if [ -z "$TAG" ]; then
|
|
echo "::error::No release tag to sync."
|
|
exit 1
|
|
fi
|
|
if [ -n "$R2_ACCOUNT_ID" ] && [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
|
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
elif [ "$GITHUB_REPOSITORY" = "farion1231/cc-switch" ]; then
|
|
# A silently stale mirror strands updater users on old versions
|
|
# (the R2 endpoint wins as long as its manifest parses), so the
|
|
# official repo must never skip this sync.
|
|
echo "::error::R2 secrets (R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY) are missing on the official repo; refusing to skip the mirror sync."
|
|
exit 1
|
|
else
|
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
echo "R2 secrets not configured; skipping download mirror sync."
|
|
fi
|
|
|
|
# Only the tag that IS GitHub's releases/latest may touch the bucket-root
|
|
# manifests or prune old versions. This keeps manual backfills of old
|
|
# tags harmless (they only restore versioned files — otherwise the root
|
|
# manifests would point at a version the prune step deletes right after)
|
|
# and makes out-of-order runs self-correcting: whichever run executes,
|
|
# only the true latest publishes the manifests.
|
|
- name: Check whether tag is the latest stable release
|
|
id: latest
|
|
if: steps.r2.outputs.configured == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# An API failure must fail the run — mapping it to "not latest"
|
|
# would skip the root manifests on a green run, silently leaving
|
|
# them stale (the exact failure mode this workflow exists to make
|
|
# loud). Retry transient errors, then give up loudly.
|
|
latest=""
|
|
for attempt in 1 2 3 4; do
|
|
latest=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name || true)
|
|
if [ -n "$latest" ]; then
|
|
break
|
|
fi
|
|
if [ "$attempt" -lt 4 ]; then
|
|
sleep $((attempt * 10))
|
|
fi
|
|
done
|
|
if [ -z "$latest" ]; then
|
|
echo "::error::Could not resolve the latest stable release from GitHub after 4 attempts; failing instead of guessing."
|
|
exit 1
|
|
fi
|
|
if [ "$TAG" = "$latest" ]; then
|
|
echo "is_latest=true" >> "$GITHUB_OUTPUT"
|
|
echo "$TAG is the latest stable release; full sync."
|
|
else
|
|
echo "is_latest=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::$TAG is not the latest stable release ($latest); syncing versioned files only — root manifests and pruning are skipped."
|
|
fi
|
|
|
|
- name: Checkout scripts
|
|
if: steps.r2.outputs.configured == 'true' && steps.latest.outputs.is_latest == 'true'
|
|
uses: actions/checkout@v6
|
|
with:
|
|
sparse-checkout: scripts
|
|
|
|
- name: Download release assets
|
|
if: steps.r2.outputs.configured == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p r2-assets
|
|
gh release download "$TAG" --dir r2-assets --repo "$GITHUB_REPOSITORY"
|
|
ls -la r2-assets
|
|
|
|
- name: Generate download manifest
|
|
if: steps.r2.outputs.configured == 'true' && steps.latest.outputs.is_latest == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
pub_date=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json publishedAt --jq .publishedAt)
|
|
node scripts/generate-download-manifest.mjs r2-assets "$TAG" "$R2_PUBLIC_BASE_URL" manifest.json "$pub_date"
|
|
|
|
- name: Rewrite updater manifest for R2
|
|
if: steps.r2.outputs.configured == 'true' && steps.latest.outputs.is_latest == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/rewrite-updater-manifest.mjs r2-assets/latest.json "$TAG" "$R2_PUBLIC_BASE_URL" latest-r2.json
|
|
|
|
- name: Upload versioned assets to R2
|
|
if: steps.r2.outputs.configured == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
# Versioned asset paths never change — cache hard at the edge.
|
|
# .tar.gz is the macOS updater payload; .sig files are not needed
|
|
# (signatures are embedded in latest.json) and latest.json goes to
|
|
# the bucket root with a short TTL instead.
|
|
aws s3 cp r2-assets "s3://$R2_BUCKET/$TAG/" \
|
|
--recursive \
|
|
--exclude "*.sig" --exclude "latest.json" \
|
|
--cache-control "public, max-age=31536000, immutable" \
|
|
--endpoint-url "$R2_ENDPOINT"
|
|
|
|
- name: Publish root manifests to R2
|
|
if: steps.r2.outputs.configured == 'true' && steps.latest.outputs.is_latest == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Re-verify right before touching the root manifests: a newer
|
|
# release may have been promoted while assets were downloading.
|
|
# Skipping then is safe — the newer release's own queued run
|
|
# publishes the newer manifests. An unresolvable API is still a
|
|
# hard failure, never a guess.
|
|
latest=""
|
|
for attempt in 1 2 3 4; do
|
|
latest=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name || true)
|
|
if [ -n "$latest" ]; then
|
|
break
|
|
fi
|
|
if [ "$attempt" -lt 4 ]; then
|
|
sleep $((attempt * 10))
|
|
fi
|
|
done
|
|
if [ -z "$latest" ]; then
|
|
echo "::error::Could not re-verify the latest stable release before publishing root manifests; failing instead of guessing."
|
|
exit 1
|
|
fi
|
|
if [ "$TAG" != "$latest" ]; then
|
|
echo "::warning::$TAG stopped being the latest stable release mid-run ($latest is); leaving root manifests untouched."
|
|
exit 0
|
|
fi
|
|
# Both manifests are replaced on every release — keep edge cache
|
|
# short. Uploaded after the assets they reference so an aborted run
|
|
# never leaves them pointing at missing files.
|
|
aws s3 cp manifest.json "s3://$R2_BUCKET/manifest.json" \
|
|
--content-type "application/json" \
|
|
--cache-control "public, max-age=300" \
|
|
--endpoint-url "$R2_ENDPOINT"
|
|
aws s3 cp latest-r2.json "s3://$R2_BUCKET/latest.json" \
|
|
--content-type "application/json" \
|
|
--cache-control "public, max-age=300" \
|
|
--endpoint-url "$R2_ENDPOINT"
|
|
|
|
- name: Prune old versions
|
|
if: steps.r2.outputs.configured == 'true' && steps.latest.outputs.is_latest == 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
versions=$(aws s3 ls "s3://$R2_BUCKET/" --endpoint-url "$R2_ENDPOINT" \
|
|
| awk '$1 == "PRE" {print $2}' | tr -d '/' | grep -E '^v[0-9]' | sort -V || true)
|
|
count=$(printf '%s\n' "$versions" | grep -c . || true)
|
|
if [ "$count" -le "$KEEP_VERSIONS" ]; then
|
|
echo "Nothing to prune ($count versions, keeping up to $KEEP_VERSIONS)."
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "$versions" | head -n "-$KEEP_VERSIONS" | while read -r old; do
|
|
[ -n "$old" ] || continue
|
|
echo "Pruning s3://$R2_BUCKET/$old/"
|
|
aws s3 rm "s3://$R2_BUCKET/$old/" --recursive --endpoint-url "$R2_ENDPOINT"
|
|
done
|