From d7768943652a4cf5fa72ca01990015ab027be58a Mon Sep 17 00:00:00 2001 From: GCWing Date: Sat, 14 Nov 2026 20:10:27 +0800 Subject: [PATCH] fix(release): align publishing with the transferred repository --- .github/workflows/cli-package.yml | 18 ++- .github/workflows/desktop-package.yml | 81 +++++++++---- .github/workflows/nightly-artifacts.yml | 2 +- .github/workflows/nightly.yml | 5 +- .github/workflows/publish-ready-desktop.yml | 12 +- docs/development/releasing.md | 31 ++++- scripts/check-github-config.test.mjs | 110 ++++++++++++++++-- scripts/desktop-tauri-build.test.mjs | 2 +- scripts/linux-binaries-manifest.test.mjs | 46 ++++++++ scripts/openbitfun-release-sync.sh | 9 +- scripts/relay/package-contract.test.mjs | 52 ++++++++- scripts/release-channel.mjs | 6 +- scripts/release-channel.test.mjs | 2 +- src/apps/cli/src/self_update.rs | 2 +- src/apps/desktop/src/api/system_api.rs | 2 +- src/apps/relay-server/Dockerfile | 2 +- src/apps/relay-server/Dockerfile.release | 4 +- src/apps/relay-server/mirror.sh | 4 +- src/apps/relay-server/release-download.sh | 71 +++++++---- .../src/remote_ssh/dispatch_ssh.rs | 6 +- 20 files changed, 374 insertions(+), 93 deletions(-) diff --git a/.github/workflows/cli-package.yml b/.github/workflows/cli-package.yml index 5bbbfb3077..0e3bcb7817 100644 --- a/.github/workflows/cli-package.yml +++ b/.github/workflows/cli-package.yml @@ -270,20 +270,26 @@ jobs: env: GH_TOKEN: ${{ secrets.HOMEBREW_TAP_DISPATCH_TOKEN }} RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} - OFFICIAL_REPOSITORY: ${{ github.repository == 'GCWing/OpenBitFun' }} + TAP_REPOSITORY: ${{ vars.HOMEBREW_TAP_REPOSITORY }} shell: bash run: | set -euo pipefail if [[ -z "${GH_TOKEN}" ]]; then - if [[ "${OFFICIAL_REPOSITORY}" == "true" ]]; then - echo "::error::HOMEBREW_TAP_DISPATCH_TOKEN is required for the official post-publication notification." + if [[ -n "${TAP_REPOSITORY}" ]]; then + echo "::error::HOMEBREW_TAP_DISPATCH_TOKEN is required for the configured tap." exit 1 fi - echo "HOMEBREW_TAP_DISPATCH_TOKEN not set on fork; skipping tap dispatch." + echo "Homebrew tap notification is not configured." exit 0 fi - echo "Dispatching openbitfun-release-published for ${RELEASE_TAG} to GCWing/homebrew-tap" + TAP_REPOSITORY="${TAP_REPOSITORY:-GCWing/homebrew-tap}" + [[ "$TAP_REPOSITORY" =~ ^[a-zA-Z0-9-]+/[a-zA-Z0-9._-]+$ && + "${TAP_REPOSITORY#*/}" != . && "${TAP_REPOSITORY#*/}" != .. ]] || { + echo "::error::HOMEBREW_TAP_REPOSITORY must be an owner/repository pair." + exit 1 + } + echo "Dispatching openbitfun-release-published for ${RELEASE_TAG} to ${TAP_REPOSITORY}" PAYLOAD="$(jq -cn --arg tag_name "${RELEASE_TAG}" '{ event_type: "openbitfun-release-published", client_payload: { @@ -295,5 +301,5 @@ jobs: -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/GCWing/homebrew-tap/dispatches \ + "${GITHUB_API_URL}/repos/${TAP_REPOSITORY}/dispatches" \ -d "$PAYLOAD" diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index bf73ff0a39..5a793cd69f 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -135,7 +135,8 @@ jobs: git fetch origin main --tags --force CHECKOUT_SHA="$(git rev-parse "${CHECKOUT_REF}^{commit}")" - if [[ "${GITHUB_REPOSITORY}" == "GCWing/OpenBitFun" ]] && \ + # The official repository ID survives an owner or name change. + if [[ "${GITHUB_REPOSITORY_ID}" == "1147866277" ]] && \ ! git merge-base --is-ancestor "${CHECKOUT_SHA}" origin/main; then echo "Ref ${CHECKOUT_REF} (${CHECKOUT_SHA}) is not part of the protected main history." >&2 exit 1 @@ -202,8 +203,8 @@ jobs: NODE_OPTIONS: --max-old-space-size=6144 OPENBITFUN_ENABLE_UPDATER_ARTIFACTS: ${{ needs.prepare.outputs.upload_to_release }} OPENBITFUN_RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} - TAURI_UPDATER_ENDPOINT: ${{ github.repository != 'GCWing/OpenBitFun' && (needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-v1-beta/latest-v1.json', github.repository) || format('https://github.com/{0}/releases/latest/download/latest-v1.json', github.repository)) || '' }} - TAURI_UPDATER_FALLBACK_ENDPOINT: ${{ github.repository != 'GCWing/OpenBitFun' && (needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-v1-beta/latest-v1.json', github.repository) || format('https://github.com/{0}/releases/latest/download/latest-v1.json', github.repository)) || '' }} + TAURI_UPDATER_ENDPOINT: ${{ github.repository_id != '1147866277' && (needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-v1-beta/latest-v1.json', github.repository) || format('https://github.com/{0}/releases/latest/download/latest-v1.json', github.repository)) || '' }} + TAURI_UPDATER_FALLBACK_ENDPOINT: ${{ github.repository_id != '1147866277' && (needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-v1-beta/latest-v1.json', github.repository) || format('https://github.com/{0}/releases/latest/download/latest-v1.json', github.repository)) || '' }} TAURI_UPDATER_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }} # Same trust root, compiled into the Desktop binary so one-click relay # deploy can verify the signed checksum locally and hand the remote host @@ -722,6 +723,10 @@ jobs: push: true provenance: false sbom: false + # Override the labels even when rebuilding a tag from before the transfer. + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} build-args: | VERSION=${{ needs.prepare.outputs.version }} REVISION=${{ needs.prepare.outputs.release_tag }} @@ -767,16 +772,6 @@ jobs: bash scripts/sign-release-assets.sh relay-image.json test -s relay-image.json.sig - - name: Upload signed image descriptor - uses: actions/upload-artifact@v6 - with: - name: openbitfun-relay-image-${{ needs.prepare.outputs.release_tag }} - if-no-files-found: error - retention-days: 7 - path: | - relay-image.json - relay-image.json.sig - # The package is private on first creation. This deliberately fails until # its visibility is changed to public, preventing an apparently green # release that anonymous customer servers cannot pull. @@ -792,6 +787,16 @@ jobs: DOCKER_CONFIG="$clean_config" docker buildx imagetools inspect \ "${IMAGE}@${IMAGE_DIGEST}" >/dev/null + - name: Upload signed image descriptor + uses: actions/upload-artifact@v6 + with: + name: openbitfun-relay-image-${{ needs.prepare.outputs.release_tag }} + if-no-files-found: error + retention-days: 7 + path: | + relay-image.json + relay-image.json.sig + - name: Attach descriptor to an existing release (image-only backfill) if: needs.prepare.outputs.relay_image_only == 'true' uses: softprops/action-gh-release@v3 @@ -890,13 +895,20 @@ jobs: run: bash scripts/sign-release-assets.sh release-manual-assets/*.exe - name: Collect updater assets + shell: bash + env: + RELEASE_SOURCE: ${{ needs.prepare.outputs.checkout_ref }} run: | - node scripts/collect-tauri-updater-assets.mjs \ - --assets-dir release-assets \ - --version "${{ needs.prepare.outputs.version }}" \ - --out-dir release-updater-assets \ - --windows-installer-dir release-manual-assets \ - --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" + set -euo pipefail + # Releases predating the maintenance installer use the signed NSIS updater. + args=(--assets-dir release-assets --version "${{ needs.prepare.outputs.version }}" + --out-dir release-updater-assets --required-platforms "${REQUIRED_UPDATER_PLATFORMS}") + if git grep -q OPENBITFUN_MAINTENANCE_V1 "$RELEASE_SOURCE" -- OpenBitFun-Installer/src-tauri/src/installer/commands.rs; then + args+=(--windows-installer-dir release-manual-assets) + elif [[ $? != 1 ]]; then + exit 1 + fi + node scripts/collect-tauri-updater-assets.mjs "${args[@]}" - name: Generate Linux binaries manifest run: | @@ -961,6 +973,30 @@ jobs: --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" \ --required-manual-platforms "windows-x86_64,darwin-aarch64,darwin-x86_64" + - name: Verify staged Relay image descriptor + shell: bash + env: + RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + ARTIFACT_RUN_IDS: ${{ inputs.artifact_run_ids }} + run: | + set -euo pipefail + owner="$(printf '%s' "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" + image="ghcr.io/${owner}/openbitfun-relay-server" + legacy_image="" + # Trusted recovery artifacts may have been signed before the repository transfer. + if [[ "$GITHUB_REPOSITORY_ID" == "1147866277" && -n "$ARTIFACT_RUN_IDS" ]]; then + legacy_image="ghcr.io/gcwing/openbitfun-relay-server" + fi + jq -e --arg tag "$RELEASE_TAG" --arg version "$RELEASE_VERSION" \ + --arg image "$image" --arg legacy "$legacy_image" ' + .schema_version == 1 and .tag == $tag and .version == $version and + (.image == $image or ($legacy != "" and .image == $legacy)) and + (.platforms | sort == ["linux/amd64", "linux/arm64"]) and + (.digest | test("^sha256:[0-9a-f]{64}$")) + ' relay-image-assets/relay-image.json >/dev/null + test -s relay-image-assets/relay-image.json.sig + - name: Stage release assets shell: bash run: | @@ -1101,11 +1137,6 @@ jobs: curl -fsSL --retry 5 --retry-delay 3 \ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \ -o relay-image.published.json - test "$(jq -r '.tag' relay-image.published.json)" = "${{ needs.prepare.outputs.release_tag }}" - test "$(jq -r '.version' relay-image.published.json)" = "${{ needs.prepare.outputs.version }}" - test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openbitfun-relay-server" - jq -e '.platforms | sort == ["linux/amd64", "linux/arm64"]' relay-image.published.json >/dev/null - jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null curl -fsSL --retry 5 --retry-delay 3 \ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \ -o relay-image.published.json.sig @@ -1205,7 +1236,7 @@ jobs: # still the source of truth, so a failed or unconfigured ping never fails # the release. Receiver: scripts/openbitfun-release-sync.sh. - name: Request openbitfun mirror sync - if: github.repository == 'GCWing/OpenBitFun' + if: github.repository_id == '1147866277' continue-on-error: true env: SYNC_WEBHOOK_URL: ${{ secrets.OPENBITFUN_SYNC_WEBHOOK_URL }} diff --git a/.github/workflows/nightly-artifacts.yml b/.github/workflows/nightly-artifacts.yml index b203245601..a1334a061d 100644 --- a/.github/workflows/nightly-artifacts.yml +++ b/.github/workflows/nightly-artifacts.yml @@ -72,7 +72,7 @@ jobs: OPENBITFUN_RELEASE_CHANNEL: nightly # Nightly does not publish a Tauri latest-v1.json feed yet. Preserve its # existing stable updater endpoints until that publishing path exists. - TAURI_UPDATER_ENDPOINT: https://github.com/GCWing/OpenBitFun/releases/latest/download/latest-v1.json + TAURI_UPDATER_ENDPOINT: https://github.com/openBitFun/OpenBitFun/releases/latest/download/latest-v1.json TAURI_UPDATER_FALLBACK_ENDPOINT: https://openbitfun.com/release/latest-v1.json # Nightly relay archives are signed too (linux-binaries.yml receives the # key), so nightly Desktop needs the same trust root to verify them. diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5ee451ccd0..cadb7e6e70 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -95,7 +95,7 @@ jobs: upload_artifacts: true # Fork validation is intentionally cache-read-only. Only the upstream # repository may seed long-lived Nightly caches. - cache_write: ${{ github.repository_owner == 'GCWing' }} + cache_write: ${{ github.repository_id == '1147866277' }} # ── Publish nightly pre-release ──────────────────────────────────── publish-nightly: @@ -169,6 +169,9 @@ jobs: push: true provenance: false sbom: false + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} build-args: | VERSION=${{ needs.check-changes.outputs.nightly_version }} REVISION=${{ github.sha }} diff --git a/.github/workflows/publish-ready-desktop.yml b/.github/workflows/publish-ready-desktop.yml index d1fc299817..2c96d5ed74 100644 --- a/.github/workflows/publish-ready-desktop.yml +++ b/.github/workflows/publish-ready-desktop.yml @@ -105,10 +105,14 @@ jobs: test "${#packages[@]}" -eq 6 bash scripts/sign-release-assets.sh "${packages[@]}" node scripts/write-minisign-public-key.mjs --out release-assets/minisign.pub - node scripts/collect-tauri-updater-assets.mjs --assets-dir release-assets \ - --version "$RELEASE_VERSION" --out-dir release-updater-assets \ - --windows-installer-dir release-manual-assets \ - --required-platforms windows-x86_64,linux-x86_64,linux-aarch64,linux-x86_64-deb,linux-aarch64-deb,linux-x86_64-rpm,linux-aarch64-rpm + args=(--assets-dir release-assets --version "$RELEASE_VERSION" --out-dir release-updater-assets + --required-platforms windows-x86_64,linux-x86_64,linux-aarch64,linux-x86_64-deb,linux-aarch64-deb,linux-x86_64-rpm,linux-aarch64-rpm) + if git grep -q OPENBITFUN_MAINTENANCE_V1 "$RELEASE_TAG" -- OpenBitFun-Installer/src-tauri/src/installer/commands.rs; then + args+=(--windows-installer-dir release-manual-assets) + elif [[ $? != 1 ]]; then + exit 1 + fi + node scripts/collect-tauri-updater-assets.mjs "${args[@]}" node scripts/generate-linux-binaries-manifest.mjs --assets-dir linux-release-assets \ --version "$RELEASE_VERSION" --tag "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ --out linux-release-assets/linux-binaries-v1.json diff --git a/docs/development/releasing.md b/docs/development/releasing.md index 0cfa292078..e8756c2f17 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -22,6 +22,35 @@ its tag automatically. Release creation and editing use GitHub CLI without `--target`: sending `target_commitish` for workflow-changing commits can require `workflows:write`, even for an existing tag. Asset upload precedes publication. +## Release infrastructure after the repository transfer + +The official repository is `openBitFun/OpenBitFun` (GitHub repository ID +`1147866277`). Official Actions use the repository ID for release-only settings +such as the protected `main` history check and download mirror webhook. New +official Relay images go to `ghcr.io/openbitfun/openbitfun-relay-server`. + +Set the organization package to public and give `openBitFun/OpenBitFun` write +access under the package's **Manage Actions access** settings. If a first push +creates the package as private, change its visibility and rerun the failed image +job. The image job checks anonymous access before the Desktop Release can +publish. Keep the public `gcwing/openbitfun-relay-server` package and its existing +digests available for older releases. + +Deploy the updated mirror script from the official checkout before the first +release in the organization namespace. The mirror accepts signed descriptors +from either official namespace and copies their bytes and signatures unchanged. +Finalizing trusted Actions artifacts from before the transfer can also reuse +their original signed descriptor and the Windows updater format built by the +original source. The standalone Relay deploy script tries the +organization package first and can retry the same tag or digest in the old +namespace. For a default `latest` request it resolves the current GitHub release +tag before retrying the old namespace. + +For the optional Homebrew notification from `CLI Package`, set +`HOMEBREW_TAP_REPOSITORY` to the tap's `owner/repository` and configure +`HOMEBREW_TAP_DISPATCH_TOKEN`. Existing token-only configurations still target +`GCWing/homebrew-tap`. + ## Recovering one macOS package For an existing immutable tag, dispatch `Desktop Package` from the current @@ -191,7 +220,7 @@ The selected ref must resolve to a commit in the protected `main` history. The workflow pins that SHA before dispatching platform jobs and rejects an existing release tag if it points somewhere else. Configure the signing secrets and the public beta approval policy so untrusted pull-request code cannot access them. -This protected-history requirement applies to the canonical `GCWing/OpenBitFun` +This protected-history requirement applies to the canonical `openBitFun/OpenBitFun` repository; forks may run packaging from their own test branches. A fork beta uses that fork's `channel-v1-beta` release as both updater origins, so it cannot silently consume or mutate the canonical beta channel. diff --git a/scripts/check-github-config.test.mjs b/scripts/check-github-config.test.mjs index 70b5a4d33c..8cc2529533 100644 --- a/scripts/check-github-config.test.mjs +++ b/scripts/check-github-config.test.mjs @@ -1079,7 +1079,7 @@ test('nightly orchestrates the shared build before the separately privileged pub build_linux_binaries: true, build_relay_image: true, upload_artifacts: true, - cache_write: "${{ github.repository_owner == 'GCWing' }}", + cache_write: "${{ github.repository_id == '1147866277' }}", }); assert.deepEqual(publish.needs, ['check-changes', 'build-artifacts']); assert.match(publish.if, /inputs\.build_only != true/); @@ -1368,6 +1368,7 @@ test('nightly publishes and verifies the Relay image in the current repository o publish?.with?.tags, `${image}:${'${{ env.NIGHTLY_TAG }}'}\n${image}:${'${{ steps.nightly-image-meta.outputs.asset_version }}'}\n`, ); + assert.match(publish?.with?.labels ?? '', /org\.opencontainers\.image\.source=\$\{\{ github\.server_url \}\}\/\$\{\{ github\.repository \}\}/); assert.equal( smoke?.run, `bash scripts/relay/smoke-image.sh \\\n "${image}@\${IMAGE_DIGEST}"\n`, @@ -1506,7 +1507,7 @@ test('Desktop packaging keeps beta identity explicit and stable-safe', () => { const prepareStep = workflow.jobs.prepare.steps.find( (step) => step.name === 'Resolve version metadata', ); - assert.match(prepareStep.run, /GITHUB_REPOSITORY.*GCWing\/OpenBitFun/); + assert.match(prepareStep.run, /GITHUB_REPOSITORY_ID.*1147866277/); assert.match(prepareStep.run, /merge-base --is-ancestor/); assert.match(prepareStep.run, /rev-parse --verify --quiet/); @@ -1515,7 +1516,7 @@ test('Desktop packaging keeps beta identity explicit and stable-safe', () => { packageJob.env.OPENBITFUN_RELEASE_CHANNEL, '${{ needs.prepare.outputs.release_channel }}', ); - assert.match(packageJob.env.TAURI_UPDATER_ENDPOINT, /github\.repository/); + assert.match(packageJob.env.TAURI_UPDATER_ENDPOINT, /github\.repository_id != '1147866277'/); assert.match(packageJob.env.TAURI_UPDATER_ENDPOINT, /channel-v1-beta/); assert.ok(packageJob.env.TAURI_UPDATER_ENDPOINT.includes("format('https://github.com/{0}/releases/latest/download/latest-v1.json', github.repository)")); assert.equal(packageJob.env.TAURI_UPDATER_ENDPOINT, packageJob.env.TAURI_UPDATER_FALLBACK_ENDPOINT); @@ -1554,6 +1555,8 @@ test('Desktop packaging keeps beta identity explicit and stable-safe', () => { ); const uploadSteps = workflow.jobs['upload-release-assets'].steps; + assert.equal(uploadSteps.find((step) => step.name === 'Request openbitfun mirror sync').if, + "github.repository_id == '1147866277'"); const release = uploadSteps.find((step) => step.name === 'Upload to release'); assert.equal( release.env.RELEASE_PRERELEASE, @@ -1670,7 +1673,7 @@ test('stable and beta publication require every producer, while artifact-only ru const steps = jobs['upload-release-assets'].steps; for (const name of [ 'Download Linux binary artifacts', 'Download Relay image descriptor', - 'Generate Linux binaries manifest', 'Stage release assets', + 'Generate Linux binaries manifest', 'Verify staged Relay image descriptor', 'Stage release assets', 'Verify published Linux binaries manifest', 'Verify published Relay image descriptor', ]) { assert.equal(steps.find((step) => step.name === name)?.if, name.startsWith('Download') ? '${{ !inputs.artifact_run_ids }}' : undefined, name); @@ -2022,12 +2025,16 @@ test('Relay image rebuild resolves the release tag instead of the newer workflow t.after(() => rmSync(root, { recursive: true, force: true })); const tagSha = 'a'.repeat(40); const headSha = 'b'.repeat(40); - for (const checkoutRef of ['', 'newer-branch']) { - const output = path.join(root, `output-${checkoutRef || 'tag'}`); + for (const [checkoutRef, protectedHistory, failure] of [ + ['', 'yes', null], ['newer-branch', 'yes', /not requested commit/], + ['', 'no', /not part of the protected main history/], + ]) { + const output = path.join(root, `output-${checkoutRef || 'tag'}-${protectedHistory}`); const result = spawnSync('bash', ['-c', ` git() { case "$1" in - fetch|merge-base) return 0 ;; + fetch) return 0 ;; + merge-base) [[ "$PROTECTED_HISTORY" == yes ]] ;; rev-parse) case "\${!#}" in 'v1.0.0^{commit}') echo "$TAG_SHA" ;; @@ -2041,15 +2048,15 @@ test('Relay image rebuild resolves the release tag instead of the newer workflow env: { ...process.env, GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_SHA: headSha, - GITHUB_REPOSITORY: 'GCWing/OpenBitFun', GITHUB_OUTPUT: output, + GITHUB_REPOSITORY: 'openBitFun/OpenBitFun', GITHUB_REPOSITORY_ID: '1147866277', GITHUB_OUTPUT: output, INPUT_TAG_NAME: 'v1.0.0', INPUT_CHECKOUT_REF: checkoutRef, INPUT_RELEASE_CHANNEL: 'stable', INPUT_UPLOAD_TO_RELEASE: 'true', - INPUT_RELAY_IMAGE_ONLY: 'true', TAG_SHA: tagSha, + INPUT_RELAY_IMAGE_ONLY: 'true', TAG_SHA: tagSha, PROTECTED_HISTORY: protectedHistory, }, }); - if (checkoutRef) { + if (failure) { assert.notEqual(result.status, 0); - assert.match(result.stderr, /not requested commit/); + assert.match(result.stderr, failure); } else { assert.equal(result.status, 0, result.stderr); assert.match(readFileSync(output, 'utf8'), new RegExp(`checkout_ref=${tagSha}`)); @@ -2057,6 +2064,85 @@ test('Relay image rebuild resolves the release tag instead of the newer workflow } }); +test('Desktop finalization selects the Windows updater format from the release source', { + skip: process.platform === 'win32', +}, () => { + const workflow = yaml.parse(readFileSync(path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8')); + const step = workflow.jobs['upload-release-assets'].steps.find((entry) => entry.name === 'Collect updater assets'); + assert.equal(step.env.RELEASE_SOURCE, '${{ needs.prepare.outputs.checkout_ref }}'); + for (const [ref, grepStatus, modern] of [['old-tag', '1', false], ['new-tag', '0', true], ['unknown', '128', false]]) { + const result = spawnSync('bash', ['-c', ` + git() { + [[ "$*" == "grep -q OPENBITFUN_MAINTENANCE_V1 $RELEASE_SOURCE -- OpenBitFun-Installer/src-tauri/src/installer/commands.rs" ]] || return 128 + return "$GREP_STATUS" + } + node() { printf '%s\n' "$@"; } + ${step.run.replaceAll('${{ needs.prepare.outputs.version }}', '1.0.10')} + `], { + cwd: repoRoot, encoding: 'utf8', windowsHide: true, + env: { ...process.env, RELEASE_SOURCE: ref, GREP_STATUS: grepStatus, REQUIRED_UPDATER_PLATFORMS: 'windows-x86_64' }, + }); + assert.equal(result.status, grepStatus === '128' ? 1 : 0, result.stderr); + assert.equal(result.stdout.includes('--windows-installer-dir\nrelease-manual-assets\n'), modern); + } +}); + +test('CLI dispatches only to a configured Homebrew tap or the historical token-only destination', { + skip: process.platform === 'win32' || spawnSync('jq', ['--version'], { windowsHide: true }).status !== 0, +}, () => { + const workflow = yaml.parse(readFileSync(path.join(repoRoot, '.github/workflows/cli-package.yml'), 'utf8')); + const step = workflow.jobs['upload-release-assets'].steps.find((entry) => entry.name === 'Notify homebrew-tap (post-publication)'); + assert.equal(step.env.TAP_REPOSITORY, '${{ vars.HOMEBREW_TAP_REPOSITORY }}'); + for (const [tap, token, status, destination] of [ + ['', '', 0, ''], ['openBitFun/homebrew-tap', '', 1, ''], + ['openBitFun/homebrew-tap', 'fixture', 0, 'openBitFun/homebrew-tap'], + ['', 'fixture', 0, 'GCWing/homebrew-tap'], ['../bad!tap', 'fixture', 1, ''], + ]) { + const result = spawnSync('bash', ['-c', `curl() { printf 'request %s\n' "$@"; }\n${step.run}`], { + cwd: repoRoot, encoding: 'utf8', windowsHide: true, + env: { ...process.env, TAP_REPOSITORY: tap, GH_TOKEN: token, + GITHUB_API_URL: 'https://api.github.com', RELEASE_TAG: 'v1.0.10' }, + }); + assert.equal(result.status, status, result.stderr); + const url = result.stdout.split('\n').find((line) => line.startsWith('request https://')); + assert.equal(url, destination ? `request https://api.github.com/repos/${destination}/dispatches` : undefined); + } +}); + +test('Relay descriptor validation allows pre-transfer images only for official artifact recovery', { + skip: process.platform === 'win32' || spawnSync('jq', ['--version'], { windowsHide: true }).status !== 0, +}, (t) => { + const workflow = yaml.parse(readFileSync(path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8')); + const steps = workflow.jobs['upload-release-assets'].steps; + const validation = steps.find((step) => step.name === 'Verify staged Relay image descriptor'); + assert.ok(steps.indexOf(validation) < steps.findIndex((step) => step.name === 'Upload to release')); + const build = workflow.jobs['publish-relay-image'].steps.find((step) => step.name === 'Build and push multi-platform image'); + assert.match(build.with.labels, /org\.opencontainers\.image\.source=\$\{\{ github\.server_url \}\}\/\$\{\{ github\.repository \}\}/); + const root = mkdtempSync(path.join(tmpdir(), 'openbitfun-release-descriptor-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + mkdirSync(path.join(root, 'relay-image-assets')); + writeFileSync(path.join(root, 'relay-image-assets/relay-image.json.sig'), 'signature fixture'); + for (const [owner, repoId, runIds, namespace, version, accepted] of [ + ['openBitFun', '1147866277', '', 'openbitfun', '1.0.10', true], + ['openBitFun', '1147866277', '123', 'gcwing', '1.0.10', true], + ['openBitFun', '1147866277', '', 'gcwing', '1.0.10', false], + ['openBitFun', '1147866277', '123', 'gcwing', '1.0.9', false], + ['fork', '1234567', '123', 'gcwing', '1.0.10', false], + ['fork', '1234567', '', 'fork', '1.0.10', true], + ]) { + writeFileSync(path.join(root, 'relay-image-assets/relay-image.json'), JSON.stringify({ + schema_version: 1, version, tag: `v${version}`, image: `ghcr.io/${namespace}/openbitfun-relay-server`, + digest: `sha256:${'a'.repeat(64)}`, platforms: ['linux/amd64', 'linux/arm64'], + })); + const result = spawnSync('bash', ['-c', validation.run], { + cwd: root, encoding: 'utf8', windowsHide: true, + env: { ...process.env, GITHUB_REPOSITORY_OWNER: owner, GITHUB_REPOSITORY_ID: repoId, + ARTIFACT_RUN_IDS: runIds, RELEASE_TAG: 'v1.0.10', RELEASE_VERSION: '1.0.10' }, + }); + assert.equal(result.status, accepted ? 0 : 1, `${owner}/${namespace} ${version} (${runIds}): ${result.stderr}`); + } +}); + test('Relay image tag selection keeps Beta and old stable backfills away from latest', { skip: process.platform === 'win32', }, (t) => { @@ -2162,7 +2248,7 @@ test('nightly and beta use the shared build-version projection', () => { assert.equal(artifacts.jobs.package.env.OPENBITFUN_RELEASE_CHANNEL, 'nightly'); assert.equal( artifacts.jobs.package.env.TAURI_UPDATER_ENDPOINT, - 'https://github.com/GCWing/OpenBitFun/releases/latest/download/latest-v1.json', + 'https://github.com/openBitFun/OpenBitFun/releases/latest/download/latest-v1.json', ); assert.equal( artifacts.jobs.package.env.TAURI_UPDATER_FALLBACK_ENDPOINT, diff --git a/scripts/desktop-tauri-build.test.mjs b/scripts/desktop-tauri-build.test.mjs index 7955b6432e..19a7534004 100644 --- a/scripts/desktop-tauri-build.test.mjs +++ b/scripts/desktop-tauri-build.test.mjs @@ -453,7 +453,7 @@ test('beta Desktop artifacts compile and bundle only beta updater endpoints', () const config = JSON.parse(readFileSync(generated, 'utf8')); assert.equal( config.plugins.updater.endpoints[0], - 'https://github.com/GCWing/OpenBitFun/releases/download/channel-v1-beta/latest-v1.json', + 'https://github.com/openBitFun/OpenBitFun/releases/download/channel-v1-beta/latest-v1.json', ); assert.equal( config.plugins.updater.endpoints[1], diff --git a/scripts/linux-binaries-manifest.test.mjs b/scripts/linux-binaries-manifest.test.mjs index c3439f6146..010ed1ca2d 100644 --- a/scripts/linux-binaries-manifest.test.mjs +++ b/scripts/linux-binaries-manifest.test.mjs @@ -206,6 +206,52 @@ test('release sync pins Relay and Linux metadata to the updater release during l ]); }); +test('the release mirror copies signed Relay descriptors from both official namespaces unchanged', (t) => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'openbitfun-relay-mirror-')); + t.after(() => fs.rmSync(temp, { recursive: true, force: true })); + for (const [namespace, version, accepted] of [ + ['gcwing', '1.0.10', true], ['openbitfun', '1.0.10', true], + ['openbitfun', '1.0.9', false], ['unrelated-owner', '1.0.10', false], + ]) { + const mirror = fs.mkdtempSync(path.join(temp, `${namespace}-`)); + fs.mkdirSync(path.join(mirror, '1.0.10')); + const descriptor = JSON.stringify({ schema_version: 1, version, tag: `v${version}`, + image: `ghcr.io/${namespace}/openbitfun-relay-server`, digest: `sha256:${'a'.repeat(64)}` }); + const result = spawnSync('bash', ['-c', ` + source "$SYNC_SCRIPT" + VERSION=1.0.10 + VERSION_DIR="$WEBSITE_RELEASE_DIR/$VERSION" + curl() { + local output + while [ "$#" -gt 1 ]; do + if [ "$1" = -o ]; then output="$2"; shift; fi + shift + done + case "$1" in + *.sig) printf 'original signature' > "$output" ;; + *) printf '%s' "$DESCRIPTOR" > "$output"; printf 200 ;; + esac + } + mirror_relay_image_descriptor + `], { + cwd: repoRoot, encoding: 'utf8', windowsHide: true, + env: { ...process.env, SYNC_SCRIPT: path.join(repoRoot, 'scripts/openbitfun-release-sync.sh'), + OPENBITFUN_RELEASE_CHANNEL: 'stable', WEBSITE_RELEASE_DIR: mirror, DESCRIPTOR: descriptor }, + }); + assert.equal(result.status, accepted ? 0 : 1, `${namespace} ${version}: ${result.stderr}`); + for (const dir of [mirror, path.join(mirror, '1.0.10')]) { + const image = path.join(dir, 'relay-image.json'); + if (accepted) { + assert.equal(fs.readFileSync(image, 'utf8'), descriptor); + assert.equal(fs.readFileSync(`${image}.sig`, 'utf8'), 'original signature'); + } else { + assert.equal(fs.existsSync(image), false); + assert.equal(fs.existsSync(`${image}.sig`), false); + } + } + } +}); + test('openbitfun sync mirrors the website installer from the exact updater release', () => { const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'openbitfun-windows-installer-mirror-')); const versionDir = path.join(temp, 'release', '1.2.3'); diff --git a/scripts/openbitfun-release-sync.sh b/scripts/openbitfun-release-sync.sh index 607d952d34..c68572cd91 100755 --- a/scripts/openbitfun-release-sync.sh +++ b/scripts/openbitfun-release-sync.sh @@ -48,11 +48,11 @@ RELEASE_CHANNEL="${OPENBITFUN_RELEASE_CHANNEL:-stable}" case "$RELEASE_CHANNEL" in stable) CHANNEL_PATH="" - GITHUB_RELEASE_ROOT="https://github.com/GCWing/OpenBitFun/releases/latest/download" + GITHUB_RELEASE_ROOT="https://github.com/openBitFun/OpenBitFun/releases/latest/download" ;; beta) CHANNEL_PATH="/beta" - GITHUB_RELEASE_ROOT="https://github.com/GCWing/OpenBitFun/releases/download/channel-v1-beta" + GITHUB_RELEASE_ROOT="https://github.com/openBitFun/OpenBitFun/releases/download/channel-v1-beta" ;; *) echo "Unsupported OPENBITFUN_RELEASE_CHANNEL: $RELEASE_CHANNEL" >&2 @@ -477,7 +477,10 @@ import json, re, sys with open(sys.argv[1], encoding="utf-8") as f: data = json.load(f) assert data.get("schema_version") == 1 -assert data.get("image") == "ghcr.io/gcwing/openbitfun-relay-server" +assert data.get("image") in { + "ghcr.io/openbitfun/openbitfun-relay-server", + "ghcr.io/gcwing/openbitfun-relay-server", +} assert re.fullmatch(r"sha256:[0-9a-f]{64}", data.get("digest", "")) print(data["version"]) PY diff --git a/scripts/relay/package-contract.test.mjs b/scripts/relay/package-contract.test.mjs index 345c1ff587..f64d922c4a 100644 --- a/scripts/relay/package-contract.test.mjs +++ b/scripts/relay/package-contract.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,6 +9,53 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../ const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'); +test('Relay deploy falls back to the same old image tag or digest and honors explicit overrides', { + skip: process.platform === 'win32', +}, () => { + const official = 'ghcr.io/openbitfun/openbitfun-relay-server'; + const legacy = 'ghcr.io/gcwing/openbitfun-relay-server'; + const override = 'ghcr.io/custom/openbitfun-relay-server'; + const digest = `sha256:${'a'.repeat(64)}`; + for (const { tag, pin = '', image = '', available, pulls, resolves = false } of [ + { tag: 'latest', available: `${official}:latest`, pulls: [`${official}:latest`] }, + { tag: 'latest', available: `${legacy}:v1.0.10`, pulls: [`${official}:latest`, `${legacy}:v1.0.10`], resolves: true }, + { tag: 'v1.0.9', available: `${legacy}:v1.0.9`, pulls: [`${official}:v1.0.9`, `${legacy}:v1.0.9`] }, + { tag: 'latest', pin: digest, available: `${legacy}@${digest}`, pulls: [`${official}@${digest}`, `${legacy}@${digest}`] }, + { tag: 'v1.0.9', image: override, available: '', pulls: [`${override}:v1.0.9`] }, + { tag: 'latest', available: '', pulls: [`${official}:latest`, `${legacy}:v1.0.10`], resolves: true }, + ]) { + const result = spawnSync('bash', ['-c', ` + set -euo pipefail + source src/apps/relay-server/release-download.sh + openbitfun_relay_native_platform() { echo linux/amd64; } + curl() { + echo 'resolve latest' >&2 + case "\${!#}" in + https://github.com/openBitFun/OpenBitFun/releases/latest) + printf 'https://github.com/openBitFun/OpenBitFun/releases/tag/v1.0.10' ;; + *) return 1 ;; + esac + } + openbitfun_image_docker_with_timeout() { + printf 'pull %s\n' "\${!#}" >&2 + [[ "\${!#}" == "$AVAILABLE_IMAGE" ]] + } + openbitfun_image_docker() { echo amd64; } + openbitfun_run_relay_image() { printf '%s %s\n' "$OPENBITFUN_RELAY_IMAGE" "$1"; } + openbitfun_try_release_deploy + `], { + cwd: repoRoot, encoding: 'utf8', windowsHide: true, + env: { ...process.env, AVAILABLE_IMAGE: available, OPENBITFUN_RELAY_IMAGE: image, + OPENBITFUN_RELAY_IMAGE_TAG: tag, OPENBITFUN_RELAY_IMAGE_DIGEST: pin, + OPENBITFUN_REQUIRE_IMAGE_DIGEST: '', OPENBITFUN_GITHUB_RELEASE_BASE: '', OPENBITFUN_MIRROR_REQUESTED_MODE: 'global' }, + }); + assert.equal(result.status, available ? 0 : 1, result.stderr); + assert.deepEqual(result.stderr.split('\n').filter((line) => line.startsWith('pull ')), pulls.map((ref) => `pull ${ref}`)); + assert.equal(result.stderr.includes('resolve latest'), resolves); + if (available) assert.equal(result.stdout.trim(), `${available.split(/[:@]/)[0]} ${available}`); + } +}); + test('relay archive contains the runtime and admin binaries plus static assets', () => { const packageScript = read('scripts/relay/package-unix.sh'); assert.match(packageScript, /openbitfun-relay-server/); @@ -67,7 +115,7 @@ test('formal and nightly releases publish signed anonymous multi-platform Relay assert.match(workflow, /platforms:\s*linux\/amd64,linux\/arm64/); assert.match( workflow, - /ghcr\.io\/(?:gcwing|\$\{GITHUB_REPOSITORY_OWNER,,\})\/openbitfun-relay-server/i, + /ghcr\.io\/\$\{GITHUB_REPOSITORY_OWNER,,\}\/openbitfun-relay-server/, ); assert.match(workflow, /relay-image\.json/); assert.match(workflow, /scripts\/sign-release-assets\.sh relay-image\.json/); @@ -83,7 +131,7 @@ test('formal and nightly releases publish signed anonymous multi-platform Relay assert.match(smoke, /docker image rm "\$IMAGE_REF"/); assert.match(smoke, /docker logs --tail/); - assert.match(dockerfile, /org\.opencontainers\.image\.source="https:\/\/github\.com\/GCWing\/OpenBitFun"/); + assert.match(dockerfile, /org\.opencontainers\.image\.source="https:\/\/github\.com\/openBitFun\/OpenBitFun"/); assert.match(dockerfile, /TARGETARCH/); assert.match(dockerfile, /openbitfun-relay-server/); assert.match(dockerfile, /relay-admin/); diff --git a/scripts/release-channel.mjs b/scripts/release-channel.mjs index be938903e7..5200f2e385 100644 --- a/scripts/release-channel.mjs +++ b/scripts/release-channel.mjs @@ -1,19 +1,19 @@ const CHANNELS = { stable: { primaryUpdaterEndpoint: - 'https://github.com/GCWing/OpenBitFun/releases/latest/download/latest-v1.json', + 'https://github.com/openBitFun/OpenBitFun/releases/latest/download/latest-v1.json', fallbackUpdaterEndpoint: 'https://openbitfun.com/release/latest-v1.json', githubChannelTag: null, }, beta: { primaryUpdaterEndpoint: - 'https://github.com/GCWing/OpenBitFun/releases/download/channel-v1-beta/latest-v1.json', + 'https://github.com/openBitFun/OpenBitFun/releases/download/channel-v1-beta/latest-v1.json', fallbackUpdaterEndpoint: 'https://openbitfun.com/release/beta/latest-v1.json', githubChannelTag: 'channel-v1-beta', }, nightly: { primaryUpdaterEndpoint: - 'https://github.com/GCWing/OpenBitFun/releases/download/nightly/latest-v1.json', + 'https://github.com/openBitFun/OpenBitFun/releases/download/nightly/latest-v1.json', fallbackUpdaterEndpoint: 'https://openbitfun.com/release/nightly/latest-v1.json', githubChannelTag: 'nightly', }, diff --git a/scripts/release-channel.test.mjs b/scripts/release-channel.test.mjs index 030ab2ed54..5330f71014 100644 --- a/scripts/release-channel.test.mjs +++ b/scripts/release-channel.test.mjs @@ -203,6 +203,6 @@ test('Desktop, CLI and SSH dispatch consume only the versioned stable feeds', () assert.doesNotMatch(source, /https:[^"\s]+\/latest\.json/); } const cli = readFileSync(new URL('../src/apps/cli/src/self_update.rs', import.meta.url), 'utf8'); - assert.ok(cli.includes('https://github.com/GCWing/OpenBitFun/releases/latest/download/linux-binaries-v1.json')); + assert.ok(cli.includes(new URL('linux-binaries-v1.json', stable.primaryUpdaterEndpoint).href)); assert.ok(cli.includes('https://openbitfun.com/release/linux-binaries-v1.json')); }); diff --git a/src/apps/cli/src/self_update.rs b/src/apps/cli/src/self_update.rs index 260b931a91..a845ae4332 100644 --- a/src/apps/cli/src/self_update.rs +++ b/src/apps/cli/src/self_update.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant, SystemTime}; use tar::Archive; const GITHUB_MANIFEST: &str = - "https://github.com/GCWing/OpenBitFun/releases/latest/download/linux-binaries-v1.json"; + "https://github.com/openBitFun/OpenBitFun/releases/latest/download/linux-binaries-v1.json"; const OPENBITFUN_MANIFEST: &str = "https://openbitfun.com/release/linux-binaries-v1.json"; const AUTO_CHECK_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60); diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index d7574065fd..c6b9351df8 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -15,7 +15,7 @@ use tauri_plugin_updater::UpdaterExt; /// `scripts/desktop-tauri-build.mjs`, which bakes the same pair into the bundle. const GITHUB_UPDATER_ENDPOINT: &str = match option_env!("OPENBITFUN_UPDATER_PRIMARY_ENDPOINT") { Some(endpoint) => endpoint, - None => "https://github.com/GCWing/OpenBitFun/releases/latest/download/latest-v1.json", + None => "https://github.com/openBitFun/OpenBitFun/releases/latest/download/latest-v1.json", }; const OPENBITFUN_UPDATER_ENDPOINT: &str = match option_env!("OPENBITFUN_UPDATER_FALLBACK_ENDPOINT") { diff --git a/src/apps/relay-server/Dockerfile b/src/apps/relay-server/Dockerfile index 4b0b931b82..b8778302e9 100644 --- a/src/apps/relay-server/Dockerfile +++ b/src/apps/relay-server/Dockerfile @@ -134,7 +134,7 @@ ARG OPENBITFUN_APT_MIRROR=mirrors.aliyun.com LABEL org.opencontainers.image.title="OpenBitFun Relay Server" \ org.opencontainers.image.description="Self-hosted relay server for OpenBitFun" \ - org.opencontainers.image.source="https://github.com/GCWing/OpenBitFun" \ + org.opencontainers.image.source="https://github.com/openBitFun/OpenBitFun" \ org.opencontainers.image.revision="${RELAY_GIT_COMMIT}" \ org.opencontainers.image.licenses="MIT" diff --git a/src/apps/relay-server/Dockerfile.release b/src/apps/relay-server/Dockerfile.release index b2047607ec..c9bf5f29d7 100644 --- a/src/apps/relay-server/Dockerfile.release +++ b/src/apps/relay-server/Dockerfile.release @@ -29,8 +29,8 @@ ARG VERSION ARG REVISION LABEL org.opencontainers.image.title="OpenBitFun Relay Server" \ org.opencontainers.image.description="Self-hosted relay server for OpenBitFun" \ - org.opencontainers.image.source="https://github.com/GCWing/OpenBitFun" \ - org.opencontainers.image.url="https://github.com/GCWing/OpenBitFun" \ + org.opencontainers.image.source="https://github.com/openBitFun/OpenBitFun" \ + org.opencontainers.image.url="https://github.com/openBitFun/OpenBitFun" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.revision="${REVISION}" \ org.opencontainers.image.licenses="MIT" diff --git a/src/apps/relay-server/mirror.sh b/src/apps/relay-server/mirror.sh index 3eb313ec66..a13a531b8e 100644 --- a/src/apps/relay-server/mirror.sh +++ b/src/apps/relay-server/mirror.sh @@ -299,8 +299,8 @@ openbitfun_mirror_resolve_mode() { } openbitfun_mirror_export_urls() { - local git_upstream="${OPENBITFUN_REPO_GIT_URL:-https://github.com/GCWing/OpenBitFun.git}" - local tarball_upstream="${OPENBITFUN_REPO_TARBALL_URL:-https://github.com/GCWing/OpenBitFun/archive/refs/heads/main.tar.gz}" + local git_upstream="${OPENBITFUN_REPO_GIT_URL:-https://github.com/openBitFun/OpenBitFun.git}" + local tarball_upstream="${OPENBITFUN_REPO_TARBALL_URL:-https://github.com/openBitFun/OpenBitFun/archive/refs/heads/main.tar.gz}" local proxy="${OPENBITFUN_GITHUB_PROXY:-https://ghfast.top/}" local docker_get_upstream="${OPENBITFUN_DOCKER_INSTALL_URL:-https://get.docker.com}" diff --git a/src/apps/relay-server/release-download.sh b/src/apps/relay-server/release-download.sh index bbb6584724..fdf84878fa 100644 --- a/src/apps/relay-server/release-download.sh +++ b/src/apps/relay-server/release-download.sh @@ -12,7 +12,7 @@ # path is intentionally gone. # # Configuration: -# OPENBITFUN_RELAY_IMAGE canonical image repository +# OPENBITFUN_RELAY_IMAGE explicit image repository override # OPENBITFUN_RELAY_IMAGE_DIGEST sha256:<64 lowercase hex> (required by Desktop) # OPENBITFUN_RELAY_IMAGE_TAG manual-script fallback tag (default release tag) # OPENBITFUN_IMAGE_PULL_TIMEOUT per-route pull timeout in seconds (default 900) @@ -22,12 +22,15 @@ # RELAY_PORT published/container port (default 9700) # RELAY_HOST_BIND_IP host bind address (default 0.0.0.0) -OPENBITFUN_RELAY_IMAGE="${OPENBITFUN_RELAY_IMAGE:-ghcr.io/gcwing/openbitfun-relay-server}" +OPENBITFUN_DEFAULT_RELAY_IMAGE="ghcr.io/openbitfun/openbitfun-relay-server" +OPENBITFUN_LEGACY_RELAY_IMAGE="ghcr.io/gcwing/openbitfun-relay-server" +OPENBITFUN_RELAY_IMAGE_OVERRIDE="${OPENBITFUN_RELAY_IMAGE:-}" +OPENBITFUN_RELAY_IMAGE="${OPENBITFUN_RELAY_IMAGE_OVERRIDE:-$OPENBITFUN_DEFAULT_RELAY_IMAGE}" OPENBITFUN_RELAY_IMAGE_TAG="${OPENBITFUN_RELAY_IMAGE_TAG:-${OPENBITFUN_RELEASE_TAG:-latest}}" OPENBITFUN_IMAGE_PULL_TIMEOUT="${OPENBITFUN_IMAGE_PULL_TIMEOUT:-900}" OPENBITFUN_GITHUB_PROBE_WINDOW="${OPENBITFUN_GITHUB_PROBE_WINDOW:-10}" OPENBITFUN_GITHUB_HEALTHY_BPS="${OPENBITFUN_GITHUB_HEALTHY_BPS:-524288}" -OPENBITFUN_GITHUB_RELEASE_BASE="${OPENBITFUN_GITHUB_RELEASE_BASE:-https://github.com/GCWing/OpenBitFun/releases}" +OPENBITFUN_GITHUB_RELEASE_BASE="${OPENBITFUN_GITHUB_RELEASE_BASE:-https://github.com/openBitFun/OpenBitFun/releases}" # The embedded Desktop helpers call Docker through openbitfun_docker; the manual # script exposes docker_cmd. Keep this file independent of either caller. @@ -92,6 +95,23 @@ openbitfun_relay_image_ref() { fi } +# Legacy `latest` stops advancing after the transfer. Resolve GitHub's current +# release tag before trying the old registry so an outage cannot install an +# older build. Explicit tag and digest requests already identify the image. +openbitfun_legacy_relay_tag() { + if [ "$OPENBITFUN_RELAY_IMAGE_TAG" != latest ] || [ -n "${OPENBITFUN_RELAY_IMAGE_DIGEST:-}" ]; then + printf '%s\n' "$OPENBITFUN_RELAY_IMAGE_TAG" + return + fi + + local release_url tag + release_url="$(curl -fsSLI --max-time 15 -o /dev/null -w '%{url_effective}' \ + "${OPENBITFUN_GITHUB_RELEASE_BASE}/latest")" || return 1 + tag="${release_url##*/releases/tag/}" + [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-beta)?$ ]] || return 1 + printf '%s\n' "$tag" +} + openbitfun_relay_archive_target() { case "$1" in linux/amd64) echo x86_64-unknown-linux-gnu ;; @@ -149,14 +169,14 @@ openbitfun_probe_github_throughput() { # remain authoritative. The digest is identical across routes, so transport # selection never changes the image Desktop authenticated. openbitfun_pull_relay_image() { - local platform="$1" routes route_name repository image_ref selected="" + local platform="$1" repository="$2" route image_ref selected="" + local routes=() local requested_mode github_speed mirror_first=0 local pull_timeout="${OPENBITFUN_IMAGE_PULL_TIMEOUT:-900}" case "$pull_timeout" in '' | *[!0-9]*) pull_timeout=900 ;; esac - routes="$(mktemp)" requested_mode="${OPENBITFUN_MIRROR_REQUESTED_MODE:-${OPENBITFUN_MIRROR_MODE:-auto}}" case "$OPENBITFUN_GITHUB_HEALTHY_BPS" in '' | *[!0-9]*) OPENBITFUN_GITHUB_HEALTHY_BPS=524288 ;; @@ -173,30 +193,22 @@ openbitfun_pull_relay_image() { esac if [ "$requested_mode" = global ]; then - printf '%s\t%s\n' "official GHCR" "$OPENBITFUN_RELAY_IMAGE" >"$routes" + routes=("$repository") elif [ "$mirror_first" = "1" ]; then - printf '%s\t%s\n' \ - "NJU GHCR accelerator" "ghcr.nju.edu.cn/${OPENBITFUN_RELAY_IMAGE#ghcr.io/}" \ - "DaoCloud GHCR accelerator" "m.daocloud.io/${OPENBITFUN_RELAY_IMAGE}" \ - "official GHCR fallback" "$OPENBITFUN_RELAY_IMAGE" >"$routes" + routes=("ghcr.nju.edu.cn/${repository#ghcr.io/}" "m.daocloud.io/${repository}" "$repository") else - printf '%s\t%s\n' \ - "official GHCR" "$OPENBITFUN_RELAY_IMAGE" \ - "NJU GHCR accelerator fallback" "ghcr.nju.edu.cn/${OPENBITFUN_RELAY_IMAGE#ghcr.io/}" \ - "DaoCloud GHCR accelerator fallback" "m.daocloud.io/${OPENBITFUN_RELAY_IMAGE}" >"$routes" + routes=("$repository" "ghcr.nju.edu.cn/${repository#ghcr.io/}" "m.daocloud.io/${repository}") fi - while IFS=$'\t' read -r route_name repository; do - [ -n "$repository" ] || continue - image_ref="$(openbitfun_relay_image_ref "$repository")" - echo ">>> Pulling Relay image via ${route_name}: ${image_ref}" >&2 + for route in "${routes[@]}"; do + image_ref="$(openbitfun_relay_image_ref "$route")" + echo ">>> Pulling Relay image: ${image_ref}" >&2 if openbitfun_image_docker_with_timeout "$pull_timeout" pull --platform "$platform" "$image_ref" >&2; then selected="$image_ref" break fi - echo ">>> ${route_name} failed or timed out; trying the next route." >&2 - done <"$routes" - rm -f "$routes" + echo ">>> ${route} failed or timed out." >&2 + done if [ -z "$selected" ]; then echo ">>> ERROR: Relay image pull failed on every ${OPENBITFUN_MIRROR_MODE:-global} route." >&2 @@ -301,7 +313,7 @@ openbitfun_run_relay_image() { } openbitfun_try_release_deploy() { - local platform image_ref + local platform image_ref legacy_tag platform="$(openbitfun_relay_native_platform)" || { echo ">>> ERROR: no published Relay image for architecture $(uname -m)." >&2 return 1 @@ -320,6 +332,19 @@ openbitfun_try_release_deploy() { # Ignore a user-level foreign-platform default. Relay one-click deployment is # deliberately native on its two supported server architectures. export DOCKER_DEFAULT_PLATFORM="$platform" - image_ref="$(openbitfun_pull_relay_image "$platform")" || return 1 + if ! image_ref="$(openbitfun_pull_relay_image "$platform" "$OPENBITFUN_RELAY_IMAGE")"; then + [ -z "$OPENBITFUN_RELAY_IMAGE_OVERRIDE" ] || return 1 + legacy_tag="$(openbitfun_legacy_relay_tag)" || { + echo ">>> ERROR: cannot resolve the current release tag for the legacy Relay registry." >&2 + return 1 + } + echo ">>> Retrying the same Relay release in the legacy registry." >&2 + image_ref="$(OPENBITFUN_RELAY_IMAGE_TAG="$legacy_tag" \ + openbitfun_pull_relay_image "$platform" "$OPENBITFUN_LEGACY_RELAY_IMAGE")" || return 1 + # The deployment labels should identify the namespace that supplied the bytes. + OPENBITFUN_RELAY_IMAGE="$OPENBITFUN_LEGACY_RELAY_IMAGE" \ + openbitfun_run_relay_image "$image_ref" "$platform" + return + fi openbitfun_run_relay_image "$image_ref" "$platform" } diff --git a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs index 5c957afe82..11cc0d0b07 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs @@ -34,10 +34,10 @@ use super::release_verify::{ use super::remote_git::shell_quote_posix; use super::types::SSHCommandOptions; -const GITHUB_RELEASE_BASE: &str = "https://github.com/GCWing/OpenBitFun/releases"; +const GITHUB_RELEASE_BASE: &str = "https://github.com/openBitFun/OpenBitFun/releases"; const OPENBITFUN_RELEASE_BASE: &str = "https://openbitfun.com/release"; const GITHUB_LATEST_MANIFEST: &str = - "https://github.com/GCWing/OpenBitFun/releases/latest/download/latest-v1.json"; + "https://github.com/openBitFun/OpenBitFun/releases/latest/download/latest-v1.json"; const OPENBITFUN_LATEST_MANIFEST: &str = "https://openbitfun.com/release/latest-v1.json"; const INSTALL_STEM: &str = "install-cli"; const INSTALL_DONE_MARKER: &str = "OPENBITFUN_DISPATCH_CLI_INSTALL_DONE"; @@ -3457,7 +3457,7 @@ mod tests { assert_eq!(sources[0].origin, ReleaseOrigin::GitHub); assert_eq!( sources[0].url, - format!("https://github.com/GCWing/OpenBitFun/releases/download/v1.2.3/{filename}") + format!("https://github.com/openBitFun/OpenBitFun/releases/download/v1.2.3/{filename}") ); assert_eq!(sources[1].origin, ReleaseOrigin::OpenBitFun); assert_eq!(