From bb92758d7cfad1e0fa6bedb4c41080ab7843d0f4 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Sun, 20 Sep 2026 18:21:43 +0800 Subject: [PATCH] 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. --- .gitattributes | 4 ++ .github/workflows/desktop-package.yml | 28 ++++---- docs/development/releasing.md | 16 +++-- scripts/check-github-config.test.mjs | 65 +++++++++++-------- .../fixtures/legacy-update-feeds/latest.json | 33 ++++++++++ .../legacy-update-feeds/linux-binaries.json | 41 ++++++++++++ scripts/product-identity-audit.mjs | 4 ++ scripts/product-identity-audit.test.mjs | 14 ++++ 8 files changed, 161 insertions(+), 44 deletions(-) create mode 100644 scripts/fixtures/legacy-update-feeds/latest.json create mode 100644 scripts/fixtures/legacy-update-feeds/linux-binaries.json diff --git a/.gitattributes b/.gitattributes index 07f6d9b0ad..fa8d4638cc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 9d31075862..217156deaa 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -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 @@ -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: | diff --git a/docs/development/releasing.md b/docs/development/releasing.md index fb7bee7948..122c26958c 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -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 diff --git a/scripts/check-github-config.test.mjs b/scripts/check-github-config.test.mjs index 9198893bdd..de213f6491 100644 --- a/scripts/check-github-config.test.mjs +++ b/scripts/check-github-config.test.mjs @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import { + copyFileSync, mkdirSync, mkdtempSync, readFileSync, @@ -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'); @@ -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 }); } }); diff --git a/scripts/fixtures/legacy-update-feeds/latest.json b/scripts/fixtures/legacy-update-feeds/latest.json new file mode 100644 index 0000000000..3e67574174 --- /dev/null +++ b/scripts/fixtures/legacy-update-feeds/latest.json @@ -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." +} \ No newline at end of file diff --git a/scripts/fixtures/legacy-update-feeds/linux-binaries.json b/scripts/fixtures/legacy-update-feeds/linux-binaries.json new file mode 100644 index 0000000000..c8b8defc2f --- /dev/null +++ b/scripts/fixtures/legacy-update-feeds/linux-binaries.json @@ -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" + } + } + } +} diff --git a/scripts/product-identity-audit.mjs b/scripts/product-identity-audit.mjs index ec8d0873aa..1f7ddbd8b6 100644 --- a/scripts/product-identity-audit.mjs +++ b/scripts/product-identity-audit.mjs @@ -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', diff --git a/scripts/product-identity-audit.test.mjs b/scripts/product-identity-audit.test.mjs index 0a32a89540..92b0a677f8 100644 --- a/scripts/product-identity-audit.test.mjs +++ b/scripts/product-identity-audit.test.mjs @@ -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}";`,