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
49 changes: 30 additions & 19 deletions .github/workflows/desktop-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -588,30 +588,41 @@ jobs:
# can fetch a key for is not verifiable.
printf '%s' "${BITFUN_SIGNING_PUBKEY}" | base64 -d >release-assets/minisign.pub

- name: Stage uniquely named release assets
shell: bash
run: |
set -euo pipefail
shopt -s globstar
node scripts/stage-github-release-assets.mjs \
--out-dir release-upload-assets \
release-updater-assets/* \
release-manual-assets/*.exe \
release-manual-assets/*.exe.sig \
release-assets/**/*.AppImage \
release-assets/**/*.AppImage.sig \
release-assets/**/*.deb \
release-assets/**/*.deb.sig \
release-assets/**/*.dmg \
release-assets/**/*.dmg.sig \
release-assets/**/*.rpm \
release-assets/**/*.rpm.sig \
release-assets/minisign.pub \
linux-release-assets/bitfun-cli-*.tar.gz \
linux-release-assets/bitfun-cli-*.tar.gz.sha256 \
linux-release-assets/bitfun-relay-server-*.tar.gz \
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256 \
linux-release-assets/*.tar.gz.sig \
linux-release-assets/*.tar.gz.sha256.sig \
linux-release-assets/linux-binaries.json \
relay-image-assets/relay-image.json \
relay-image-assets/relay-image.json.sig

- name: Upload to release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.prepare.outputs.release_tag }}
generate_release_notes: true
files: |
release-updater-assets/*
release-manual-assets/*.exe
release-manual-assets/*.exe.sig
release-assets/**/*.AppImage
release-assets/**/*.deb
release-assets/**/*.dmg
release-assets/**/*.rpm
release-assets/**/*.sig
release-assets/minisign.pub
linux-release-assets/bitfun-cli-*.tar.gz
linux-release-assets/bitfun-cli-*.tar.gz.sha256
linux-release-assets/bitfun-relay-server-*.tar.gz
linux-release-assets/bitfun-relay-server-*.tar.gz.sha256
linux-release-assets/*.tar.gz.sig
linux-release-assets/*.tar.gz.sha256.sig
linux-release-assets/linux-binaries.json
relay-image-assets/relay-image.json
relay-image-assets/relay-image.json.sig
files: release-upload-assets/*
fail_on_unmatched_files: true

- name: Verify published updater manifest
Expand Down
28 changes: 28 additions & 0 deletions scripts/check-github-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,31 @@ test('passes the verification key when signing the versioned Windows installer',
'release signatures must be self-verified with the configured public key',
);
});

test('stages unique release asset names before publishing', () => {
const workflow = yaml.parse(
readFileSync(
path.join(repoRoot, '.github/workflows/desktop-package.yml'),
'utf8',
),
);
const steps = workflow.jobs['upload-release-assets'].steps;
const stagingIndex = steps.findIndex(
(step) => step.name === 'Stage uniquely named release assets',
);
const uploadIndex = steps.findIndex((step) => step.name === 'Upload to release');

assert.notEqual(stagingIndex, -1);
assert.notEqual(uploadIndex, -1);
assert.ok(stagingIndex < uploadIndex);
assert.match(
steps[stagingIndex].run,
/node scripts\/stage-github-release-assets\.mjs/,
);
assert.doesNotMatch(
steps[stagingIndex].run,
/release-assets\/\*\*\/\*\.sig(?:\s|\\)/,
'raw updater signatures have colliding names across macOS architectures',
);
assert.equal(steps[uploadIndex].with.files, 'release-upload-assets/*');
});
59 changes: 59 additions & 0 deletions scripts/stage-github-release-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env node

import {
copyFileSync,
mkdirSync,
rmSync,
statSync,
} from 'node:fs';
import path from 'node:path';

const args = process.argv.slice(2);
const outDirIndex = args.indexOf('--out-dir');
if (outDirIndex === -1 || !args[outDirIndex + 1]) {
fail('Missing required --out-dir argument');
}

const outDir = path.resolve(args[outDirIndex + 1]);
const inputs = args.filter(
(_, index) => index !== outDirIndex && index !== outDirIndex + 1,
);

if (inputs.length === 0) {
fail('No release assets were provided');
}

const byName = new Map();
for (const input of inputs) {
const source = path.resolve(input);
let stats;
try {
stats = statSync(source);
} catch {
fail(`Release asset was not found: ${input}`);
}
if (!stats.isFile()) {
fail(`Release asset is not a file: ${input}`);
}

const name = path.basename(source);
const previous = byName.get(name);
if (previous) {
fail(`Duplicate release asset name ${name}: ${previous} conflicts with ${source}`);
}
byName.set(name, source);
}

rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });

for (const [name, source] of byName) {
copyFileSync(source, path.join(outDir, name));
}

console.log(`Staged ${byName.size} uniquely named GitHub release assets in ${outDir}`);

function fail(message) {
console.error(`[stage-release-assets] ${message}`);
process.exit(1);
}
43 changes: 43 additions & 0 deletions scripts/tauri-release-manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,49 @@ test('latest.json keeps the updater URL separate from the manual installer URL',
assert.equal(verified.status, 0, verified.stderr);
});

test('stages GitHub release assets in a flat directory', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-release-assets-'));
const first = path.join(temp, 'updater', 'latest.json');
const second = path.join(temp, 'manual', 'installer.exe');
const out = path.join(temp, 'staged');
fs.mkdirSync(path.dirname(first), { recursive: true });
fs.mkdirSync(path.dirname(second), { recursive: true });
fs.writeFileSync(first, 'manifest');
fs.writeFileSync(second, 'installer');

const result = run('scripts/stage-github-release-assets.mjs', [
'--out-dir', out,
first,
second,
]);

assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(out, 'latest.json'), 'utf8'), 'manifest');
assert.equal(fs.readFileSync(path.join(out, 'installer.exe'), 'utf8'), 'installer');
});

test('rejects duplicate GitHub release asset names before upload', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-release-duplicates-'));
const first = path.join(temp, 'macos-x64', 'BitFun.app.tar.gz.sig');
const second = path.join(temp, 'macos-arm64', 'BitFun.app.tar.gz.sig');
const out = path.join(temp, 'staged');
fs.mkdirSync(path.dirname(first), { recursive: true });
fs.mkdirSync(path.dirname(second), { recursive: true });
fs.writeFileSync(first, 'x64-signature');
fs.writeFileSync(second, 'arm64-signature');

const result = run('scripts/stage-github-release-assets.mjs', [
'--out-dir', out,
first,
second,
]);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /Duplicate release asset name BitFun\.app\.tar\.gz\.sig/);
assert.match(result.stderr, /macos-x64/);
assert.match(result.stderr, /macos-arm64/);
});

function run(script, args) {
return spawnSync(process.execPath, [script, ...args], {
cwd: root,
Expand Down