Skip to content

Commit 332f640

Browse files
authored
Merge pull request #2194 from wgqqqqq/fix/release-asset-name-collisions
fix(release): reject duplicate asset names before upload
2 parents e63084b + 8fd0ae3 commit 332f640

4 files changed

Lines changed: 160 additions & 19 deletions

File tree

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -588,30 +588,41 @@ jobs:
588588
# can fetch a key for is not verifiable.
589589
printf '%s' "${BITFUN_SIGNING_PUBKEY}" | base64 -d >release-assets/minisign.pub
590590
591+
- name: Stage uniquely named release assets
592+
shell: bash
593+
run: |
594+
set -euo pipefail
595+
shopt -s globstar
596+
node scripts/stage-github-release-assets.mjs \
597+
--out-dir release-upload-assets \
598+
release-updater-assets/* \
599+
release-manual-assets/*.exe \
600+
release-manual-assets/*.exe.sig \
601+
release-assets/**/*.AppImage \
602+
release-assets/**/*.AppImage.sig \
603+
release-assets/**/*.deb \
604+
release-assets/**/*.deb.sig \
605+
release-assets/**/*.dmg \
606+
release-assets/**/*.dmg.sig \
607+
release-assets/**/*.rpm \
608+
release-assets/**/*.rpm.sig \
609+
release-assets/minisign.pub \
610+
linux-release-assets/bitfun-cli-*.tar.gz \
611+
linux-release-assets/bitfun-cli-*.tar.gz.sha256 \
612+
linux-release-assets/bitfun-relay-server-*.tar.gz \
613+
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256 \
614+
linux-release-assets/*.tar.gz.sig \
615+
linux-release-assets/*.tar.gz.sha256.sig \
616+
linux-release-assets/linux-binaries.json \
617+
relay-image-assets/relay-image.json \
618+
relay-image-assets/relay-image.json.sig
619+
591620
- name: Upload to release
592621
uses: softprops/action-gh-release@v3
593622
with:
594623
tag_name: ${{ needs.prepare.outputs.release_tag }}
595624
generate_release_notes: true
596-
files: |
597-
release-updater-assets/*
598-
release-manual-assets/*.exe
599-
release-manual-assets/*.exe.sig
600-
release-assets/**/*.AppImage
601-
release-assets/**/*.deb
602-
release-assets/**/*.dmg
603-
release-assets/**/*.rpm
604-
release-assets/**/*.sig
605-
release-assets/minisign.pub
606-
linux-release-assets/bitfun-cli-*.tar.gz
607-
linux-release-assets/bitfun-cli-*.tar.gz.sha256
608-
linux-release-assets/bitfun-relay-server-*.tar.gz
609-
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256
610-
linux-release-assets/*.tar.gz.sig
611-
linux-release-assets/*.tar.gz.sha256.sig
612-
linux-release-assets/linux-binaries.json
613-
relay-image-assets/relay-image.json
614-
relay-image-assets/relay-image.json.sig
625+
files: release-upload-assets/*
615626
fail_on_unmatched_files: true
616627

617628
- name: Verify published updater manifest

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,31 @@ test('passes the verification key when signing the versioned Windows installer',
318318
'release signatures must be self-verified with the configured public key',
319319
);
320320
});
321+
322+
test('stages unique release asset names before publishing', () => {
323+
const workflow = yaml.parse(
324+
readFileSync(
325+
path.join(repoRoot, '.github/workflows/desktop-package.yml'),
326+
'utf8',
327+
),
328+
);
329+
const steps = workflow.jobs['upload-release-assets'].steps;
330+
const stagingIndex = steps.findIndex(
331+
(step) => step.name === 'Stage uniquely named release assets',
332+
);
333+
const uploadIndex = steps.findIndex((step) => step.name === 'Upload to release');
334+
335+
assert.notEqual(stagingIndex, -1);
336+
assert.notEqual(uploadIndex, -1);
337+
assert.ok(stagingIndex < uploadIndex);
338+
assert.match(
339+
steps[stagingIndex].run,
340+
/node scripts\/stage-github-release-assets\.mjs/,
341+
);
342+
assert.doesNotMatch(
343+
steps[stagingIndex].run,
344+
/release-assets\/\*\*\/\*\.sig(?:\s|\\)/,
345+
'raw updater signatures have colliding names across macOS architectures',
346+
);
347+
assert.equal(steps[uploadIndex].with.files, 'release-upload-assets/*');
348+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env node
2+
3+
import {
4+
copyFileSync,
5+
mkdirSync,
6+
rmSync,
7+
statSync,
8+
} from 'node:fs';
9+
import path from 'node:path';
10+
11+
const args = process.argv.slice(2);
12+
const outDirIndex = args.indexOf('--out-dir');
13+
if (outDirIndex === -1 || !args[outDirIndex + 1]) {
14+
fail('Missing required --out-dir argument');
15+
}
16+
17+
const outDir = path.resolve(args[outDirIndex + 1]);
18+
const inputs = args.filter(
19+
(_, index) => index !== outDirIndex && index !== outDirIndex + 1,
20+
);
21+
22+
if (inputs.length === 0) {
23+
fail('No release assets were provided');
24+
}
25+
26+
const byName = new Map();
27+
for (const input of inputs) {
28+
const source = path.resolve(input);
29+
let stats;
30+
try {
31+
stats = statSync(source);
32+
} catch {
33+
fail(`Release asset was not found: ${input}`);
34+
}
35+
if (!stats.isFile()) {
36+
fail(`Release asset is not a file: ${input}`);
37+
}
38+
39+
const name = path.basename(source);
40+
const previous = byName.get(name);
41+
if (previous) {
42+
fail(`Duplicate release asset name ${name}: ${previous} conflicts with ${source}`);
43+
}
44+
byName.set(name, source);
45+
}
46+
47+
rmSync(outDir, { recursive: true, force: true });
48+
mkdirSync(outDir, { recursive: true });
49+
50+
for (const [name, source] of byName) {
51+
copyFileSync(source, path.join(outDir, name));
52+
}
53+
54+
console.log(`Staged ${byName.size} uniquely named GitHub release assets in ${outDir}`);
55+
56+
function fail(message) {
57+
console.error(`[stage-release-assets] ${message}`);
58+
process.exit(1);
59+
}

‎scripts/tauri-release-manifest.test.mjs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,49 @@ test('latest.json keeps the updater URL separate from the manual installer URL',
7575
assert.equal(verified.status, 0, verified.stderr);
7676
});
7777

78+
test('stages GitHub release assets in a flat directory', () => {
79+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-release-assets-'));
80+
const first = path.join(temp, 'updater', 'latest.json');
81+
const second = path.join(temp, 'manual', 'installer.exe');
82+
const out = path.join(temp, 'staged');
83+
fs.mkdirSync(path.dirname(first), { recursive: true });
84+
fs.mkdirSync(path.dirname(second), { recursive: true });
85+
fs.writeFileSync(first, 'manifest');
86+
fs.writeFileSync(second, 'installer');
87+
88+
const result = run('scripts/stage-github-release-assets.mjs', [
89+
'--out-dir', out,
90+
first,
91+
second,
92+
]);
93+
94+
assert.equal(result.status, 0, result.stderr);
95+
assert.equal(fs.readFileSync(path.join(out, 'latest.json'), 'utf8'), 'manifest');
96+
assert.equal(fs.readFileSync(path.join(out, 'installer.exe'), 'utf8'), 'installer');
97+
});
98+
99+
test('rejects duplicate GitHub release asset names before upload', () => {
100+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-release-duplicates-'));
101+
const first = path.join(temp, 'macos-x64', 'BitFun.app.tar.gz.sig');
102+
const second = path.join(temp, 'macos-arm64', 'BitFun.app.tar.gz.sig');
103+
const out = path.join(temp, 'staged');
104+
fs.mkdirSync(path.dirname(first), { recursive: true });
105+
fs.mkdirSync(path.dirname(second), { recursive: true });
106+
fs.writeFileSync(first, 'x64-signature');
107+
fs.writeFileSync(second, 'arm64-signature');
108+
109+
const result = run('scripts/stage-github-release-assets.mjs', [
110+
'--out-dir', out,
111+
first,
112+
second,
113+
]);
114+
115+
assert.notEqual(result.status, 0);
116+
assert.match(result.stderr, /Duplicate release asset name BitFun\.app\.tar\.gz\.sig/);
117+
assert.match(result.stderr, /macos-x64/);
118+
assert.match(result.stderr, /macos-arm64/);
119+
});
120+
78121
function run(script, args) {
79122
return spawnSync(process.execPath, [script, ...args], {
80123
cwd: root,

0 commit comments

Comments
 (0)