desktop-release add: evidence-linux / evidence-windows files (run, commit, sizes, hashes, scan) and a check that every artifact of a version comes from one commit (--commit against the folder's other evidence); docs: check the downloads against the run before add
This commit is contained in:
parent
0b6c8cd7a0
commit
86cd79dd45
3 changed files with 49 additions and 8 deletions
|
|
@ -163,11 +163,16 @@ Then on the Mac:
|
|||
|
||||
```sh
|
||||
bun tools/desktop-release.ts add --out ~/.storebound-releases/desktop/0.1.0 \
|
||||
--commit <the run's full commit hash> --run <the run's URL> \
|
||||
storebound_0.1.0_amd64.AppImage storebound_0.1.0_amd64.deb storebound_0.1.0_x64-setup.exe
|
||||
```
|
||||
|
||||
It scans and signs each file, renames the unsigned Windows installer to `-unsigned`, and updates latest.json
|
||||
(`windows-x86_64`, `linux-x86_64`) and SHA256SUMS. For the scan, install `squashfs` and `sevenzip` from Homebrew.
|
||||
(`windows-x86_64`, `linux-x86_64`) and SHA256SUMS. It writes `evidence-linux-<version>.json` and
|
||||
`evidence-windows-<version>.json` (run, commit, sizes, hashes, the scan; they stay local like the macOS one) and
|
||||
refuses a `--commit` other than the one in the folder's macOS evidence: every artifact of a version comes from one
|
||||
commit. Before `add`, check the downloads against the run: the Linux artifact carries SHA256SUMS; for the Windows
|
||||
installer compare with `Get-FileHash` of `C:\storebound-target\release\bundle\nsis\` on the runner. For the scan, install `squashfs` and `sevenzip` from Homebrew.
|
||||
|
||||
### Runner setup
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test';
|
|||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { artifactName, developerIdIdentity, mergeManifest, pathFreeEnv, releaseUrl, remoteSessionReason, sliceProblems, sumsText, uploadable, archProblems } from '../tools/desktop-release';
|
||||
import { artifactName, developerIdIdentity, mergeManifest, pathFreeEnv, releaseUrl, remoteSessionReason, sliceProblems, sumsText, uploadable, archProblems, commitProblems } from '../tools/desktop-release';
|
||||
import { COPIES, setVersion, versions } from '../tools/desktop-version';
|
||||
|
||||
describe('desktop version authority', () => {
|
||||
|
|
@ -83,7 +83,14 @@ describe('desktop release', () => {
|
|||
expect(sumsText([{ file: 'b', sha256: '2' }, { file: 'a', sha256: '1' }])).toBe('1 a\n2 b\n');
|
||||
expect(uploadable('storebound_0.1.0_universal-unsigned.dmg')).toBe(true);
|
||||
expect(uploadable('latest.json')).toBe(true);
|
||||
for (const n of ['evidence-macos-0.1.0.json', '.DS_Store', 'latest.json.part', 'a b.dmg']) expect(uploadable(n)).toBe(false);
|
||||
for (const n of ['evidence-macos-0.1.0.json', 'evidence-linux-0.1.0.json', '.DS_Store', 'latest.json.part', 'a b.dmg']) expect(uploadable(n)).toBe(false);
|
||||
});
|
||||
test('add: every artifact of a version comes from one commit', () => {
|
||||
const a = 'a'.repeat(40), b = 'b'.repeat(40);
|
||||
expect(commitProblems(a, { 'evidence-macos-0.1.0.json': a, 'evidence-linux-0.1.0.json': undefined })).toEqual([]);
|
||||
expect(commitProblems(undefined, { 'evidence-macos-0.1.0.json': a })).toEqual([]);
|
||||
expect(commitProblems(b, { 'evidence-macos-0.1.0.json': a })[0]).toContain('evidence-macos-0.1.0.json says commit aaaaaaaaaa');
|
||||
expect(commitProblems('25258a8', {})[0]).toContain('40-character');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@
|
|||
// The built window is booted headless first (tools/desktop-boot-check.ts). Then, on the exact artifacts: codesign / Gatekeeper expectations, lipo (every executable universal; the x86_64 slice is
|
||||
// never run, Rosetta), the native self-test (sidecar handshake) from the mounted DMG and from the unpacked updater bundle, the privacy scan, minisign signatures with
|
||||
// the updater key (~/.storebound-keys/updater.key), SHA256SUMS, latest.json and evidence.json.
|
||||
// bun tools/desktop-release.ts add --out <dir> <files...> [--code-signed]
|
||||
// bun tools/desktop-release.ts add --out <dir> [--commit <sha>] [--run <url>] <files...> [--code-signed]
|
||||
// Adds Windows / Linux installers built by the Forgejo workflow (.forgejo/workflows/desktop.yml) to the version
|
||||
// folder: privacy scan, minisign signature here (the key never goes to a runner), latest.json, SHA256SUMS.
|
||||
// folder: privacy scan, minisign signature here (the key never goes to a runner), latest.json, SHA256SUMS and
|
||||
// evidence-<linux|windows>-<version>.json (the run and commit they came from; a commit other than the one of the
|
||||
// folder's other evidence is refused: every artifact of a version comes from one commit).
|
||||
// bun tools/desktop-release.ts publish --out <dir> --host <user@host> [--channel stable|beta] [--yes]
|
||||
// Checks the folder again, uploads it to the server's incoming folder (rsync) and runs the root-owned publish
|
||||
// script there (tmp -> mv, latest.json last). Without --yes it only prints the plan (dry run).
|
||||
|
|
@ -538,6 +540,14 @@ Unsigned preview (ad-hoc). To ship a signed, notarized build:
|
|||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// add (Windows / Linux installers from the Forgejo workflow)
|
||||
|
||||
/** Every artifact of a version comes from one commit: the commit of the added files must match the commit in the
|
||||
* folder's other evidence files (pure; `evidence` maps an evidence file name to its commit). */
|
||||
export function commitProblems(commit: string | undefined, evidence: Record<string, string | undefined>): string[] {
|
||||
if (!commit) return [];
|
||||
if (!/^[0-9a-f]{40}$/.test(commit)) return [`--commit: a full 40-character commit hash, got ${commit}`];
|
||||
return Object.entries(evidence).filter(([, c]) => c && c !== commit).map(([f, c]) => `${f} says commit ${c!.slice(0, 10)}, these files are from ${commit.slice(0, 10)}`);
|
||||
}
|
||||
|
||||
function add(argv: string[]): void {
|
||||
const a = args(argv);
|
||||
const { version, drift } = versions();
|
||||
|
|
@ -546,10 +556,16 @@ function add(argv: string[]): void {
|
|||
const files = a.rest.slice(1);
|
||||
if (!files.length) fail('usage: add --out <dir> <setup.exe | .AppImage | .deb ...>');
|
||||
mkdirSync(out, { recursive: true });
|
||||
const commit = a.get('commit');
|
||||
const otherEvidence = Object.fromEntries(readdirSync(out).filter(n => /^evidence-.*\.json$/.test(n))
|
||||
.map(n => [n, (JSON.parse(readFileSync(join(out, n), 'utf8')) as { commit?: string }).commit]));
|
||||
const commitBad = commitProblems(commit, otherEvidence);
|
||||
if (commitBad.length) fail(`not one commit: ${commitBad.join('; ')}`);
|
||||
const codeSigned = a.has('code-signed');
|
||||
const platforms: Manifest['platforms'] = {};
|
||||
const downloads: Partial<Record<DownloadKey, Download>> = {};
|
||||
const added: string[] = [];
|
||||
const sources: Record<string, string> = {};
|
||||
for (const f of files.map(x => resolve(x))) {
|
||||
const n = basename(f);
|
||||
if (!n.includes(`_${version}_`)) fail(`${n} is not version ${version}`);
|
||||
|
|
@ -559,8 +575,9 @@ function add(argv: string[]): void {
|
|||
const dest = join(out, name);
|
||||
copyFileSync(f, dest);
|
||||
added.push(dest);
|
||||
sources[name] = n;
|
||||
}
|
||||
privacyGate(added);
|
||||
const privacy = privacyGate(added);
|
||||
for (const dest of added) {
|
||||
const name = basename(dest);
|
||||
const sig = signFile(dest);
|
||||
|
|
@ -571,8 +588,20 @@ function add(argv: string[]): void {
|
|||
say(`added ${name} (${mb(d.size)})`);
|
||||
}
|
||||
const existing = readManifest(out);
|
||||
writeVersionFiles(out, mergeManifest(existing, version, a.get('notes') ?? existing?.notes ?? `storebound desktop ${version}`, new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'), { platforms, downloads }));
|
||||
say(`latest.json now has ${Object.keys(readManifest(out)!.platforms).join(', ')}`);
|
||||
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
writeVersionFiles(out, mergeManifest(existing, version, a.get('notes') ?? existing?.notes ?? `storebound desktop ${version}`, now, { platforms, downloads }));
|
||||
// evidence per platform (stays local, like the macOS one): where the files came from and what was checked here
|
||||
for (const platform of ['linux', 'windows'] as const) {
|
||||
const mine = added.filter(d => (platform === 'windows') === d.endsWith('.exe'));
|
||||
if (!mine.length) continue;
|
||||
const evidence = {
|
||||
product: 'storebound desktop', version, platform: `${platform}-x86_64`, addedAt: now, commit: commit ?? null, run: a.get('run') ?? null,
|
||||
checks: { privacy, updaterSignature: 'minisign, verified against tauri.conf.json', codeSigned: platform === 'windows' ? codeSigned : false },
|
||||
artifacts: mine.map(d => ({ file: basename(d), from: sources[basename(d)], size: statSync(d).size, sha256: sha256(d) })),
|
||||
};
|
||||
writeFileSync(join(out, `evidence-${platform}-${version}.json`), `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
}
|
||||
say(`latest.json now has ${Object.keys(readManifest(out)!.platforms).join(', ')}${commit ? `; evidence: commit ${commit.slice(0, 10)}` : ''}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue