Skip to content

Commit 8ac55b9

Browse files
committed
ci(release): pin legacy update feeds to the 0.2.20 notice manifests
Every stable release re-downloaded latest.json and linux-binaries.json from the v0.2.19 tag, which silently erased the 0.2.20 upgrade notice after each new 1.x tag. Copy the pinned manifests from scripts/fixtures/legacy-update-feeds/ instead and validate them inline (desktop feed = 0.2.20 with non-empty notes, CLI feed = 0.2.19). The publication step byte-compares the uploaded feeds against the fixtures so a drifting fixture or stale upload fails the run. The step no longer shells out to curl or jq, and the verify step is unrolled per manifest so it runs on any bash environment without relying on loop-variable expansion.
1 parent ea2c534 commit 8ac55b9

6 files changed

Lines changed: 143 additions & 44 deletions

File tree

‎.gitattributes‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ src/apps/mobile/**/*.swift text eol=lf
3838
# The Product Operation Registry embeds and compares this generated JSON byte for byte.
3939
src/crates/contracts/product-domains/src/generated/remote-surface-registry.json text eol=lf
4040

41+
# Release workflows byte-compare these pinned legacy updater feeds against the
42+
# uploaded assets, so their newlines must survive every checkout unchanged.
43+
scripts/fixtures/legacy-update-feeds/*.json text eol=lf
44+
4145
# models.dev provenance hashes exact redistributed bytes. Keep these assets
4246
# stable across checkout platforms so the offline release check is reproducible.
4347
src/crates/services/services-integrations/assets/models-dev.json text eol=lf

‎.github/workflows/desktop-package.yml‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -772,18 +772,18 @@ jobs:
772772
relay-image-assets/relay-image.json.sig
773773
774774
# Old Desktop and CLI clients follow GitHub Latest. Keep their feeds on
775-
# the final 0.2 release; only versioned manifests may advertise 1.x.
775+
# the pinned legacy manifests checked into the repository; only versioned
776+
# manifests may advertise 1.x.
776777
- name: Preserve legacy update feeds
777778
if: needs.prepare.outputs.release_channel == 'stable'
778779
shell: bash
779780
run: |
780781
set -euo pipefail
781-
for manifest in latest.json linux-binaries.json; do
782-
curl -fsSL --retry 5 --retry-delay 3 \
783-
"https://github.com/GCWing/OpenBitFun/releases/download/v0.2.19/${manifest}" \
784-
-o "release-upload-assets/${manifest}"
785-
jq -e '.version == "0.2.19"' "release-upload-assets/${manifest}" >/dev/null
786-
done
782+
cp scripts/fixtures/legacy-update-feeds/latest.json \
783+
release-upload-assets/latest.json
784+
cp scripts/fixtures/legacy-update-feeds/linux-binaries.json \
785+
release-upload-assets/linux-binaries.json
786+
node -e 'const fs=require("node:fs");for(const f of ["release-upload-assets/latest.json","release-upload-assets/linux-binaries.json"]){const m=JSON.parse(fs.readFileSync(f,"utf8"));if(f.endsWith("latest.json")){if(m.version!=="0.2.20"||typeof m.notes!=="string"||m.notes.length===0)process.exit(1);}else if(m.version!=="0.2.19")process.exit(1);}'
787787
788788
- name: Upload to release
789789
shell: bash
@@ -826,12 +826,14 @@ jobs:
826826
shell: bash
827827
run: |
828828
set -euo pipefail
829-
for manifest in latest.json linux-binaries.json; do
830-
curl -fsSL --retry 5 --retry-delay 3 \
831-
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/${manifest}" \
832-
-o "legacy-${manifest}"
833-
cmp "release-upload-assets/${manifest}" "legacy-${manifest}"
834-
done
829+
curl -fsSL --retry 5 --retry-delay 3 \
830+
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/latest.json" \
831+
-o legacy-latest.json
832+
cmp release-upload-assets/latest.json legacy-latest.json
833+
curl -fsSL --retry 5 --retry-delay 3 \
834+
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
835+
-o legacy-linux-binaries.json
836+
cmp release-upload-assets/linux-binaries.json legacy-linux-binaries.json
835837
836838
- name: Verify published updater manifest
837839
run: |

‎docs/development/releasing.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ its tag automatically. Release creation and editing use GitHub CLI without
2626

2727
Desktop 1.x reads `latest-v1.json`; CLI 1.x reads `linux-binaries-v1.json`,
2828
from GitHub Latest or `/release/` on the mirror. Every stable release also carries
29-
unchanged `latest.json` and `linux-binaries.json` from **v0.2.19**, the final
30-
legacy release. Publication rejects legacy feeds whose version is not 0.2.19.
31-
Thus installed 0.2.x clients continue to see only 0.2.x, even after GitHub Latest
32-
moves to 1.x. Do not rename the 1.x manifests back to the legacy filenames.
29+
the pinned legacy feeds `latest.json` and `linux-binaries.json` from
30+
`scripts/fixtures/legacy-update-feeds/`. The desktop feed carries version 0.2.20
31+
with a release note that points 0.2.x users to the manual 1.x download and the
32+
Data Migrator; its `platforms` block still resolves to the final 0.2.19
33+
artifacts, so the notice never installs 1.x into a 0.2.x client. The CLI feed
34+
stays byte-for-byte on 0.2.19 because the legacy CLI manifest has no note field.
35+
Preservation validates the copied manifests with an inline Node check (the
36+
desktop feed must be version 0.2.20 with non-empty notes; the CLI feed must be
37+
0.2.19) and the publication step byte-compares the uploaded feeds against the
38+
pinned fixtures, so a drifting fixture or a stale upload fails the run. Do not
39+
rename the 1.x manifests back to the legacy
40+
filenames, and keep the pinned fixtures' `platforms` signatures unchanged.
3341

3442
The mirror writes only the versioned 1.x feeds and keeps the two newest
3543
version directories. It does not retain 0.2.x artifact trees; 0.2.x clients

‎scripts/check-github-config.test.mjs‎

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from 'node:assert/strict';
22
import {
3+
copyFileSync,
34
mkdirSync,
45
mkdtempSync,
56
readFileSync,
@@ -1488,7 +1489,7 @@ test('Linux Rust workflows do not install an unused native OpenSSL toolchain', (
14881489
});
14891490

14901491

1491-
test('public beta launch uses Latest while legacy updater bytes stay on 0.2.19', () => {
1492+
test('public beta launch uses Latest while legacy updater feeds stay on the pinned notice manifest', () => {
14921493
const workflow = yaml.parse(readFileSync(path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8'));
14931494
const steps = workflow.jobs['upload-release-assets'].steps;
14941495
const upload = steps.find((step) => step.name === 'Upload to release');
@@ -1498,33 +1499,43 @@ test('public beta launch uses Latest while legacy updater bytes stay on 0.2.19',
14981499
assert.equal(preserve.if, "needs.prepare.outputs.release_channel == 'stable'");
14991500
assert.ok(steps.indexOf(preserve) < steps.indexOf(upload));
15001501
assert.match(steps.find((step) => step.name === 'Generate updater manifest').run, /--out release-updater-assets\/latest-v1\.json/);
1501-
for (const version of ['0.2.19', '1.0.0-beta']) {
1502-
const cwd = mkdtempSync(path.join(tmpdir(), 'openbitfun-legacy-feed-'));
1503-
try {
1504-
mkdirSync(path.join(cwd, 'release-upload-assets'));
1505-
const candidate = '{"version":"1.0.0-beta"}';
1506-
writeFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), candidate);
1507-
const legacy = JSON.stringify({ version, platforms: { 'windows-x86_64': { url: 'https://example.test/legacy.exe', signature: 'unchanged' } } });
1508-
writeFileSync(path.join(cwd, 'legacy.json'), legacy);
1509-
const result = spawnSync('bash', ['-c', `
1510-
curl() {
1511-
while [[ "$1" != "-o" ]]; do shift; done
1512-
cp legacy.json "$2"
1513-
}
1514-
${preserve.run}
1515-
`], { cwd, encoding: 'utf8', windowsHide: true });
1516-
if (version === '0.2.19') {
1517-
assert.equal(result.status, 0, result.stderr);
1518-
for (const name of ['latest.json', 'linux-binaries.json']) {
1519-
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets', name), 'utf8'), legacy);
1520-
}
1521-
} else {
1522-
assert.notEqual(result.status, 0, '1.x must never enter a legacy feed');
1523-
}
1524-
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), 'utf8'), candidate);
1525-
} finally {
1526-
rmSync(cwd, { recursive: true, force: true });
1502+
// The pinned legacy manifests must come from the repository, so the notice
1503+
// manifest survives every future release without manual re-upload.
1504+
assert.match(preserve.run, /scripts\/fixtures\/legacy-update-feeds/);
1505+
assert.doesNotMatch(preserve.run, /curl/);
1506+
assert.match(preserve.run, /0\.2\.20/);
1507+
assert.match(preserve.run, /0\.2\.19/);
1508+
assert.match(preserve.run, /notes/);
1509+
// The pinned step downloads nothing, so the released version can never leak
1510+
// into a legacy feed; the sandbox only proves the copy + guard behavior.
1511+
const cwd = mkdtempSync(path.join(tmpdir(), 'openbitfun-legacy-feed-'));
1512+
try {
1513+
// The step reads its pinned manifests via a repo-root-relative path and
1514+
// writes into release-upload-assets/, so mirror the CI working tree
1515+
// inside the sandbox instead of running against the real checkout.
1516+
mkdirSync(path.join(cwd, 'scripts/fixtures/legacy-update-feeds'), { recursive: true });
1517+
mkdirSync(path.join(cwd, 'release-upload-assets'));
1518+
for (const name of ['latest.json', 'linux-binaries.json']) {
1519+
copyFileSync(
1520+
path.join(repoRoot, 'scripts/fixtures/legacy-update-feeds', name),
1521+
path.join(cwd, 'scripts/fixtures/legacy-update-feeds', name),
1522+
);
1523+
}
1524+
const candidate = '{"version":"1.0.0-beta"}';
1525+
writeFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), candidate);
1526+
const result = spawnSync('bash', ['-c', preserve.run], {
1527+
cwd, encoding: 'utf8', windowsHide: true,
1528+
});
1529+
assert.equal(result.status, 0, result.stderr);
1530+
for (const name of ['latest.json', 'linux-binaries.json']) {
1531+
assert.equal(
1532+
readFileSync(path.join(cwd, 'release-upload-assets', name), 'utf8'),
1533+
readFileSync(path.join(repoRoot, 'scripts/fixtures/legacy-update-feeds', name), 'utf8'),
1534+
);
15271535
}
1536+
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), 'utf8'), candidate);
1537+
} finally {
1538+
rmSync(cwd, { recursive: true, force: true });
15281539
}
15291540
});
15301541

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"version": "0.2.20",
3+
"pub_date": "2026-08-28T18:44:04.013Z",
4+
"platforms": {
5+
"darwin-aarch64": {
6+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVVBEQ2I5UEhtZVpqcGV5d0VwMkFXWTM0NVJSTWFOaDJuaXlEU3JWSmZHTHgwTHYxNGJFa0xkRk5RY29OQ29SVEtVVTQ4ZUtBZVdQQi9nbTFrSEFBTFFVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQwMTEyCWZpbGU6Qml0RnVuLmFwcC50YXIuZ3oKL2VWSjdQTGo1OG5MRXc4ZDJDeTVpT1R1ZXJiM1JaNkQvK2JiL2VCd1hhYktyUkliL0I0b25uZGd3S0ZLVWFpcWZOL0V4RE1OWmxjc3FIaXNrWE1WQ0E9PQo=",
7+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_darwin-aarch64.app.tar.gz"
8+
},
9+
"darwin-x86_64": {
10+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVVBZZ05RTitIMlVrOC9MRzlSSVhVVEsxLytsTGlKR01uRFBpby9sYjdxOFZuVFdQSTdrR2NRSEN5Zk04SVZpSVpzcDdxL2NXYjU0UU9PemRJNURqTkFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQyMzU0CWZpbGU6Qml0RnVuLmFwcC50YXIuZ3oKN0hLbXBQMTEwdlNUMk9za3hyai9PWTlZQWk1U1RMQ0RLeTVrNWR4RFh1TVFDVzlrQnFQVmVBQ1lqMG1SQlBwcndoRWoxTFJQbTBJQlRDeFNFN21OQVE9PQo=",
11+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_darwin-x86_64.app.tar.gz"
12+
},
13+
"linux-aarch64": {
14+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVU9zSVlkQmw2elhNU3VIZGhTYzBHc1VDcGtscUUxT0lWRXhTcXcrcjlJVW01VnBsK1FOTldUZ0wrSFlUSXRUTnl6RmtaVFV0MEt2OCsydk10R1pkV0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTM5NTY3CWZpbGU6Qml0RnVuXzAuMi4xOV9hYXJjaDY0LkFwcEltYWdlCllIQkk2bUZQRjU1NHVxb3VwdnFZV0pkZTd4TVArT0p6VzVORFI0Zm1JQ2laUm1VVVFKRjlZQjJsSmJIM01nMkRGeWRJRW5DcGtvaEw0dTJRaUU1SkNBPT0K",
15+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_linux-aarch64.AppImage"
16+
},
17+
"linux-x86_64": {
18+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVUFDUjZEanVGc0t3RVJzR2FIT3pKWTZWUGZ2MXVuNWxoQnVzTWNkYnZjUkFCT1N6cnhZMmdBc1VMdVJENGxjREhiYnB4U3hQV3Jua3hVQUUyU0Q2dlE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQwNjQ3CWZpbGU6Qml0RnVuXzAuMi4xOV9hbWQ2NC5BcHBJbWFnZQpURlB6NGRvRFZzR3p5VXJGTVpOZmZydThiT1JIeTN4NERPRStKdSttaWlzRXcyU3FsM2p4VXY5NTB1SXNmR1FwczF4MDM0S0dJVExaMHh1TG5CMjBEZz09Cg==",
19+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_linux-x86_64.AppImage"
20+
},
21+
"windows-x86_64": {
22+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVU44Z0RCZjNqdGxmTWN2d1hEM2pubWU5YWFQdDBWVWU1bktqT1hvSXJ1djhBMXV1bmw3L3ZYUVpKZWF3RktaaUVwd09yWEtsK1pFdkJRbmFIcXdxNVFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQxMDIyCWZpbGU6Qml0RnVuXzAuMi4xOV94NjQtc2V0dXAuZXhlCmRKblNlTUpvb0FXM2gvWXlkOENqNUpla2VrdzgxTzI1K3JtaGlzOGFid01JaDZMNTRkYktrbzNSUThtelJKdFMvQnZ3V2JDQmNPQXN3SEFuZFVKU0J3PT0K",
23+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-setup.exe"
24+
}
25+
},
26+
"manual_installers": {
27+
"windows-x86_64": {
28+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-installer.exe",
29+
"signature_url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-installer.exe.sig"
30+
}
31+
},
32+
"notes": "全新版本上线!\nOpenBitFun 1.0.x 正式发布,欢迎升级!\n新版本无法从当前版本直接更新,手动下载地址:\n 官网 https://www.openbitfun.com/download\n GitHub https://github.com/GCWing/OpenBitFun/releases\n重要:两个版本数据相互独立,需使用官方数据迁移工具:\n https://github.com/GCWing/OpenBitFun/releases/tag/data-migrator-v0.1.1\n注意:\n1. 请勿点击下方「后台下载」按钮。\n2. 点击「跳过此版本」即可关闭本提示,不影响继续使用。\n3. 官网访问异常时,请在 VPN 工具中将 *.openbitfun.com 加入白名单。\n\nNew version available!\nOpenBitFun 1.0.x is now officially released — you are welcome to upgrade!\nThis new version cannot be updated directly from the current one. Download manually at:\n Official site: https://www.openbitfun.com/download\n GitHub: https://github.com/GCWing/OpenBitFun/releases\nImportant: the two versions keep their data completely separate. Use the official\nData Migrator to carry your data over:\n https://github.com/GCWing/OpenBitFun/releases/tag/data-migrator-v0.1.1\nNotes:\n1. Do NOT click the \"Download in background\" button below.\n2. Click \"Skip this version\" to dismiss this notice — it does not affect your\n continued use of the app.\n3. If the official site is unreachable, whitelist *.openbitfun.com in your VPN tool."
33+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"schemaVersion": 1,
3+
"version": "0.2.19",
4+
"tag": "v0.2.19",
5+
"platforms": {
6+
"linux-x86_64": {
7+
"target": "x86_64-unknown-linux-gnu",
8+
"cli": {
9+
"filename": "bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz",
10+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz",
11+
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz.sha256",
12+
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz.sha256.sig",
13+
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz.sig"
14+
},
15+
"relay": {
16+
"filename": "bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz",
17+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz",
18+
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sha256",
19+
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sha256.sig",
20+
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sig"
21+
}
22+
},
23+
"linux-aarch64": {
24+
"target": "aarch64-unknown-linux-gnu",
25+
"cli": {
26+
"filename": "bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz",
27+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz",
28+
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sha256",
29+
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sha256.sig",
30+
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sig"
31+
},
32+
"relay": {
33+
"filename": "bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz",
34+
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz",
35+
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sha256",
36+
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sha256.sig",
37+
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sig"
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)