From 47c13cc89da0c9fc2fd4a056b106b20064a38e48 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Sun, 6 Sep 2026 01:28:22 +0800 Subject: [PATCH] fix(ci): retry beta channel manifest content validation --- .github/workflows/desktop-package.yml | 22 ++++++++--- scripts/check-github-config.test.mjs | 53 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 034fbeec6b..564d26f195 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -824,6 +824,7 @@ jobs: GH_TOKEN: ${{ github.token }} CHECKOUT_REF: ${{ needs.prepare.outputs.checkout_ref }} CHANNEL_EXISTS: ${{ steps.beta-channel.outputs.channel_exists }} + CANDIDATE_VERSION: ${{ steps.beta-channel.outputs.candidate_version }} run: | set -euo pipefail if [[ "${CHANNEL_EXISTS}" != "true" ]]; then @@ -839,11 +840,22 @@ jobs: gh release upload channel-beta beta-channel/latest.json \ --repo "${GITHUB_REPOSITORY}" \ --clobber - curl -fsSL --retry 5 --retry-delay 3 \ - "https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-beta/latest.json" \ - -o channel-beta.published.json - test "$(jq -r '.version' channel-beta.published.json)" = \ - "${{ steps.beta-channel.outputs.candidate_version }}" + # A successful HTTP response can still contain the previous CDN object. + # Retry content validation as well as transport failures after replacement. + for attempt in {1..12}; do + if curl -fsSL --max-time 30 \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-beta/latest.json" \ + -o channel-beta.published.json && \ + jq -e --arg version "${CANDIDATE_VERSION}" \ + '.version == $version' channel-beta.published.json >/dev/null; then + echo "Beta channel manifest verified: ${CANDIDATE_VERSION}" + exit 0 + fi + echo "Beta channel manifest is not ready (attempt ${attempt}/12)." + if [[ "${attempt}" -lt 12 ]]; then sleep 5; fi + done + echo "Beta channel manifest did not converge to ${CANDIDATE_VERSION}." >&2 + exit 1 # Nudge the openbitfun.com mirror to sync now instead of on its next # 10-minute cron tick. Until the mirror has these bytes, CN clients have diff --git a/scripts/check-github-config.test.mjs b/scripts/check-github-config.test.mjs index a2a186c506..c8f2630c44 100644 --- a/scripts/check-github-config.test.mjs +++ b/scripts/check-github-config.test.mjs @@ -1230,6 +1230,59 @@ test('beta publishing cannot advance the Relay latest image tag', () => { assert.doesNotMatch(imageTags.run, /RELEASE_PRERELEASE/); }); +test('beta channel readback retries stale content and fails if it never converges', { + 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 step = workflow.jobs['upload-release-assets'].steps.find( + (entry) => entry.name === 'Publish beta channel manifest', + ); + assert.equal(step.env.CANDIDATE_VERSION, '${{ steps.beta-channel.outputs.candidate_version }}'); + const root = mkdtempSync(path.join(tmpdir(), 'openbitfun-beta-readback-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const bin = path.join(root, 'bin'); + mkdirSync(bin); + for (const command of ['gh', 'sleep']) { + writeFileSync(path.join(bin, command), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); + } + writeFileSync(path.join(bin, 'curl'), `#!/usr/bin/env node +const fs = require('node:fs'); +const count = fs.existsSync('requests') ? Number(fs.readFileSync('requests', 'utf8')) + 1 : 1; +fs.writeFileSync('requests', String(count)); +if (process.env.READBACK_CASE === 'transport' && count === 1) process.exit(22); +const output = process.argv[process.argv.indexOf('-o') + 1]; +const content = process.env.READBACK_CASE === 'malformed' && count === 1 + ? 'not json' + : JSON.stringify({ version: process.env.READBACK_CASE === 'stale' || count === 1 ? '0.2.19-beta.1' : '1.0.0-beta.1' }); +fs.writeFileSync(output, content); +`, { mode: 0o755 }); + for (const scenario of ['converges', 'transport', 'malformed', 'stale']) { + const cwd = path.join(root, scenario); + mkdirSync(cwd); + writeFileSync(path.join(cwd, 'latest.published.json'), '{"version":"1.0.0-beta.1"}'); + const result = spawnSync('bash', ['-c', step.run], { + cwd, + env: { + ...process.env, + PATH: `${bin}${path.delimiter}${process.env.PATH}`, + READBACK_CASE: scenario, + CHANNEL_EXISTS: 'true', + GITHUB_REPOSITORY: 'test/repo', + CANDIDATE_VERSION: '1.0.0-beta.1', + }, + encoding: 'utf8', + timeout: 10000, + windowsHide: true, + }); + assert.equal(result.status, scenario === 'stale' ? 1 : 0, `${scenario}: ${result.stderr}`); + const requests = Number(readFileSync(path.join(cwd, 'requests'), 'utf8')); + assert.ok(requests > 1 && requests <= 12, `${scenario}: bounded content retries`); + if (scenario === 'stale') assert.match(result.stderr, /did not converge/); + } +}); + test('nightly and beta use the shared build-version projection', () => { const artifacts = yaml.parse( readFileSync(