Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ src/apps/mobile/**/*.swift text eol=lf
# The Product Operation Registry embeds and compares this generated JSON byte for byte.
src/crates/contracts/product-domains/src/generated/remote-surface-registry.json text eol=lf

# Release workflows byte-compare these pinned legacy updater feeds against the
# uploaded assets, so their newlines must survive every checkout unchanged.
scripts/fixtures/legacy-update-feeds/*.json text eol=lf

# models.dev provenance hashes exact redistributed bytes. Keep these assets
# stable across checkout platforms so the offline release check is reproducible.
src/crates/services/services-integrations/assets/models-dev.json text eol=lf
Expand Down
28 changes: 15 additions & 13 deletions .github/workflows/desktop-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -772,18 +772,18 @@ jobs:
relay-image-assets/relay-image.json.sig

# Old Desktop and CLI clients follow GitHub Latest. Keep their feeds on
# the final 0.2 release; only versioned manifests may advertise 1.x.
# the pinned legacy manifests checked into the repository; only versioned
# manifests may advertise 1.x.
- name: Preserve legacy update feeds
if: needs.prepare.outputs.release_channel == 'stable'
shell: bash
run: |
set -euo pipefail
for manifest in latest.json linux-binaries.json; do
curl -fsSL --retry 5 --retry-delay 3 \
"https://github.com/GCWing/OpenBitFun/releases/download/v0.2.19/${manifest}" \
-o "release-upload-assets/${manifest}"
jq -e '.version == "0.2.19"' "release-upload-assets/${manifest}" >/dev/null
done
cp scripts/fixtures/legacy-update-feeds/latest.json \
release-upload-assets/latest.json
cp scripts/fixtures/legacy-update-feeds/linux-binaries.json \
release-upload-assets/linux-binaries.json
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);}'

- name: Upload to release
shell: bash
Expand Down Expand Up @@ -826,12 +826,14 @@ jobs:
shell: bash
run: |
set -euo pipefail
for manifest in latest.json linux-binaries.json; do
curl -fsSL --retry 5 --retry-delay 3 \
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/${manifest}" \
-o "legacy-${manifest}"
cmp "release-upload-assets/${manifest}" "legacy-${manifest}"
done
curl -fsSL --retry 5 --retry-delay 3 \
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/latest.json" \
-o legacy-latest.json
cmp release-upload-assets/latest.json legacy-latest.json
curl -fsSL --retry 5 --retry-delay 3 \
"https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
-o legacy-linux-binaries.json
cmp release-upload-assets/linux-binaries.json legacy-linux-binaries.json

- name: Verify published updater manifest
run: |
Expand Down
16 changes: 12 additions & 4 deletions docs/development/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ its tag automatically. Release creation and editing use GitHub CLI without

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

The mirror writes only the versioned 1.x feeds and keeps the two newest
version directories. It does not retain 0.2.x artifact trees; 0.2.x clients
Expand Down
65 changes: 38 additions & 27 deletions scripts/check-github-config.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
Expand Down Expand Up @@ -1488,7 +1489,7 @@ test('Linux Rust workflows do not install an unused native OpenSSL toolchain', (
});


test('public beta launch uses Latest while legacy updater bytes stay on 0.2.19', () => {
test('public beta launch uses Latest while legacy updater feeds stay on the pinned notice manifest', () => {
const workflow = yaml.parse(readFileSync(path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8'));
const steps = workflow.jobs['upload-release-assets'].steps;
const upload = steps.find((step) => step.name === 'Upload to release');
Expand All @@ -1498,33 +1499,43 @@ test('public beta launch uses Latest while legacy updater bytes stay on 0.2.19',
assert.equal(preserve.if, "needs.prepare.outputs.release_channel == 'stable'");
assert.ok(steps.indexOf(preserve) < steps.indexOf(upload));
assert.match(steps.find((step) => step.name === 'Generate updater manifest').run, /--out release-updater-assets\/latest-v1\.json/);
for (const version of ['0.2.19', '1.0.0-beta']) {
const cwd = mkdtempSync(path.join(tmpdir(), 'openbitfun-legacy-feed-'));
try {
mkdirSync(path.join(cwd, 'release-upload-assets'));
const candidate = '{"version":"1.0.0-beta"}';
writeFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), candidate);
const legacy = JSON.stringify({ version, platforms: { 'windows-x86_64': { url: 'https://example.test/legacy.exe', signature: 'unchanged' } } });
writeFileSync(path.join(cwd, 'legacy.json'), legacy);
const result = spawnSync('bash', ['-c', `
curl() {
while [[ "$1" != "-o" ]]; do shift; done
cp legacy.json "$2"
}
${preserve.run}
`], { cwd, encoding: 'utf8', windowsHide: true });
if (version === '0.2.19') {
assert.equal(result.status, 0, result.stderr);
for (const name of ['latest.json', 'linux-binaries.json']) {
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets', name), 'utf8'), legacy);
}
} else {
assert.notEqual(result.status, 0, '1.x must never enter a legacy feed');
}
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), 'utf8'), candidate);
} finally {
rmSync(cwd, { recursive: true, force: true });
// The pinned legacy manifests must come from the repository, so the notice
// manifest survives every future release without manual re-upload.
assert.match(preserve.run, /scripts\/fixtures\/legacy-update-feeds/);
assert.doesNotMatch(preserve.run, /curl/);
assert.match(preserve.run, /0\.2\.20/);
assert.match(preserve.run, /0\.2\.19/);
assert.match(preserve.run, /notes/);
// The pinned step downloads nothing, so the released version can never leak
// into a legacy feed; the sandbox only proves the copy + guard behavior.
const cwd = mkdtempSync(path.join(tmpdir(), 'openbitfun-legacy-feed-'));
try {
// The step reads its pinned manifests via a repo-root-relative path and
// writes into release-upload-assets/, so mirror the CI working tree
// inside the sandbox instead of running against the real checkout.
mkdirSync(path.join(cwd, 'scripts/fixtures/legacy-update-feeds'), { recursive: true });
mkdirSync(path.join(cwd, 'release-upload-assets'));
for (const name of ['latest.json', 'linux-binaries.json']) {
copyFileSync(
path.join(repoRoot, 'scripts/fixtures/legacy-update-feeds', name),
path.join(cwd, 'scripts/fixtures/legacy-update-feeds', name),
);
}
const candidate = '{"version":"1.0.0-beta"}';
writeFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), candidate);
const result = spawnSync('bash', ['-c', preserve.run], {
cwd, encoding: 'utf8', windowsHide: true,
});
assert.equal(result.status, 0, result.stderr);
for (const name of ['latest.json', 'linux-binaries.json']) {
assert.equal(
readFileSync(path.join(cwd, 'release-upload-assets', name), 'utf8'),
readFileSync(path.join(repoRoot, 'scripts/fixtures/legacy-update-feeds', name), 'utf8'),
);
}
assert.equal(readFileSync(path.join(cwd, 'release-upload-assets/latest-v1.json'), 'utf8'), candidate);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});

Expand Down
33 changes: 33 additions & 0 deletions scripts/fixtures/legacy-update-feeds/latest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"version": "0.2.20",
"pub_date": "2026-08-28T18:44:04.013Z",
"platforms": {
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVVBEQ2I5UEhtZVpqcGV5d0VwMkFXWTM0NVJSTWFOaDJuaXlEU3JWSmZHTHgwTHYxNGJFa0xkRk5RY29OQ29SVEtVVTQ4ZUtBZVdQQi9nbTFrSEFBTFFVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQwMTEyCWZpbGU6Qml0RnVuLmFwcC50YXIuZ3oKL2VWSjdQTGo1OG5MRXc4ZDJDeTVpT1R1ZXJiM1JaNkQvK2JiL2VCd1hhYktyUkliL0I0b25uZGd3S0ZLVWFpcWZOL0V4RE1OWmxjc3FIaXNrWE1WQ0E9PQo=",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_darwin-aarch64.app.tar.gz"
},
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVVBZZ05RTitIMlVrOC9MRzlSSVhVVEsxLytsTGlKR01uRFBpby9sYjdxOFZuVFdQSTdrR2NRSEN5Zk04SVZpSVpzcDdxL2NXYjU0UU9PemRJNURqTkFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQyMzU0CWZpbGU6Qml0RnVuLmFwcC50YXIuZ3oKN0hLbXBQMTEwdlNUMk9za3hyai9PWTlZQWk1U1RMQ0RLeTVrNWR4RFh1TVFDVzlrQnFQVmVBQ1lqMG1SQlBwcndoRWoxTFJQbTBJQlRDeFNFN21OQVE9PQo=",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_darwin-x86_64.app.tar.gz"
},
"linux-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVU9zSVlkQmw2elhNU3VIZGhTYzBHc1VDcGtscUUxT0lWRXhTcXcrcjlJVW01VnBsK1FOTldUZ0wrSFlUSXRUTnl6RmtaVFV0MEt2OCsydk10R1pkV0FBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTM5NTY3CWZpbGU6Qml0RnVuXzAuMi4xOV9hYXJjaDY0LkFwcEltYWdlCllIQkk2bUZQRjU1NHVxb3VwdnFZV0pkZTd4TVArT0p6VzVORFI0Zm1JQ2laUm1VVVFKRjlZQjJsSmJIM01nMkRGeWRJRW5DcGtvaEw0dTJRaUU1SkNBPT0K",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_linux-aarch64.AppImage"
},
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVUFDUjZEanVGc0t3RVJzR2FIT3pKWTZWUGZ2MXVuNWxoQnVzTWNkYnZjUkFCT1N6cnhZMmdBc1VMdVJENGxjREhiYnB4U3hQV3Jua3hVQUUyU0Q2dlE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQwNjQ3CWZpbGU6Qml0RnVuXzAuMi4xOV9hbWQ2NC5BcHBJbWFnZQpURlB6NGRvRFZzR3p5VXJGTVpOZmZydThiT1JIeTN4NERPRStKdSttaWlzRXcyU3FsM2p4VXY5NTB1SXNmR1FwczF4MDM0S0dJVExaMHh1TG5CMjBEZz09Cg==",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_linux-x86_64.AppImage"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSMm84QnN2bnowVU44Z0RCZjNqdGxmTWN2d1hEM2pubWU5YWFQdDBWVWU1bktqT1hvSXJ1djhBMXV1bmw3L3ZYUVpKZWF3RktaaUVwd09yWEtsK1pFdkJRbmFIcXdxNVFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3OTQxMDIyCWZpbGU6Qml0RnVuXzAuMi4xOV94NjQtc2V0dXAuZXhlCmRKblNlTUpvb0FXM2gvWXlkOENqNUpla2VrdzgxTzI1K3JtaGlzOGFid01JaDZMNTRkYktrbzNSUThtelJKdFMvQnZ3V2JDQmNPQXN3SEFuZFVKU0J3PT0K",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-setup.exe"
}
},
"manual_installers": {
"windows-x86_64": {
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-installer.exe",
"signature_url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/BitFun_0.2.19_windows-x86_64-installer.exe.sig"
}
},
"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."
}
41 changes: 41 additions & 0 deletions scripts/fixtures/legacy-update-feeds/linux-binaries.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"schemaVersion": 1,
"version": "0.2.19",
"tag": "v0.2.19",
"platforms": {
"linux-x86_64": {
"target": "x86_64-unknown-linux-gnu",
"cli": {
"filename": "bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz",
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz.sha256",
"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",
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-x86_64-unknown-linux-gnu.tar.gz.sig"
},
"relay": {
"filename": "bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz",
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sha256",
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sha256.sig",
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz.sig"
}
},
"linux-aarch64": {
"target": "aarch64-unknown-linux-gnu",
"cli": {
"filename": "bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz",
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sha256",
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sha256.sig",
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-cli-0.2.19-aarch64-unknown-linux-gnu.tar.gz.sig"
},
"relay": {
"filename": "bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz",
"url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz",
"sha256Url": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sha256",
"sha256SigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sha256.sig",
"sigUrl": "https://github.com/GCWing/BitFun/releases/download/v0.2.19/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz.sig"
}
}
}
}
4 changes: 4 additions & 0 deletions scripts/product-identity-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const retiredIdentityDataBoundaryPrefixes = Object.freeze([
'src/crates/assembly/core/src/legacy_migration/',
'src/crates/services/legacy-migration/',
'src/crates/services/legacy-migration-adapters/',
// Pinned legacy updater feeds reproduce the historical 0.2.x manifests
// (download URLs and file names included) so old clients keep resolving
// their last real release; they must not be rewritten to the new identity.
'scripts/fixtures/legacy-update-feeds/',
]);
const noncanonicalIdentityDataBoundaryFiles = new Set([
'OPENBITFUN_LEGACY_DATA_MIGRATION_INVENTORY.md',
Expand Down
14 changes: 14 additions & 0 deletions scripts/product-identity-audit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ test('limits retired identity data to the one-time production migration boundary
),
[],
);
assert.deepEqual(
violationsFor(
`{"url": "https://github.com/example/${retiredLowerName}/releases/download/v0.2.19/${retiredLowerName}_0.2.19_windows-x86_64-setup.exe"}`,
'scripts/fixtures/legacy-update-feeds/latest.json',
),
[],
);
assert.equal(
violationsFor(
`const sourceLabel = "${retiredName}";`,
'scripts/fixtures/example.json',
).length,
1,
);
assert.equal(
violationsFor(
`const SOURCE_PRODUCT: &str = "${retiredLowerName}";`,
Expand Down
Loading