release candidates: draft assets are read by their attachment uuid (the tag URL is 404 for drafts: the record job could not merge SHA256SUMS); the installers are scanned with the private terms in their build jobs, so the record job needs no apt tools (it spent 5 minutes installing them)
This commit is contained in:
parent
cdaecc2afb
commit
6873f2cc68
5 changed files with 34 additions and 23 deletions
|
|
@ -6,7 +6,7 @@
|
|||
# tarball with real piped stdin, Linux and Windows x Node 20 / Node 22 / Bun.
|
||||
# linux AppImage + deb (path-free build), self-tests, both started under Xvfb, privacy scan.
|
||||
# windows NSIS installer, silent per-user install, self-test, uninstall, the Node prompt smoke, privacy scan.
|
||||
# record when all are green: the draft release rc-<sha> with the installers, the acceptance runs and matrix and
|
||||
# record when all are green (the installers already scanned with the private terms): the draft release rc-<sha> with the installers, the acceptance runs and matrix and
|
||||
# green.json (tools/release-candidate.ts record). A v* tag on this commit only promotes it (release.yml).
|
||||
# gate decides first: a commit that differs from its green parent only in version copies + CHANGELOG (a release's
|
||||
# version bump, proven file by file: tools/release-candidate.ts gate) reuses the parent's ci and acceptance results
|
||||
|
|
@ -200,8 +200,14 @@ jobs:
|
|||
}
|
||||
smoke AppImage "$GITHUB_WORKSPACE/out/storebound_${V}_amd64.AppImage"
|
||||
smoke deb /tmp/deb/usr/bin/storebound-desktop
|
||||
- name: Privacy scan
|
||||
run: bun tools/privacy-scan.ts out/
|
||||
- name: Privacy scan (with the private terms; never printed)
|
||||
env:
|
||||
FORBIDDEN_TERMS: ${{ secrets.FORBIDDEN_TERMS }}
|
||||
run: |
|
||||
[ -n "$FORBIDDEN_TERMS" ] || { echo "secret FORBIDDEN_TERMS is missing" >&2; exit 1; }
|
||||
umask 077; printf '%s\n' "$FORBIDDEN_TERMS" > "$RUNNER_TEMP/terms.txt"
|
||||
rc=0; STOREBOUND_FORBIDDEN_TERMS="$RUNNER_TEMP/terms.txt" bun tools/privacy-scan.ts out/ || rc=$?
|
||||
rm -f "$RUNNER_TEMP/terms.txt"; exit $rc
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-linux
|
||||
|
|
@ -254,9 +260,19 @@ jobs:
|
|||
& $exe --self-test | Tee-Object -Variable st
|
||||
if ($LASTEXITCODE -ne 0) { throw "self-test failed" }
|
||||
Start-Process -Wait -FilePath "$env:LOCALAPPDATA\storebound\uninstall.exe" -ArgumentList '/S'
|
||||
- name: Privacy scan (no .pdb, no paths)
|
||||
- name: Privacy scan (no .pdb, no paths; with the private terms, never printed)
|
||||
shell: pwsh
|
||||
run: bun tools/privacy-scan.ts out
|
||||
env:
|
||||
FORBIDDEN_TERMS: ${{ secrets.FORBIDDEN_TERMS }}
|
||||
run: |
|
||||
if (-not $env:FORBIDDEN_TERMS) { throw "secret FORBIDDEN_TERMS is missing" }
|
||||
$t = Join-Path $env:RUNNER_TEMP "terms-$([guid]::NewGuid()).txt"
|
||||
Set-Content -Path $t -Value $env:FORBIDDEN_TERMS -NoNewline:$false
|
||||
$env:STOREBOUND_FORBIDDEN_TERMS = $t
|
||||
bun tools/privacy-scan.ts out
|
||||
$rc = $LASTEXITCODE
|
||||
Remove-Item -Force $t
|
||||
if ($rc -ne 0) { throw "privacy scan failed" }
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storebound-desktop-windows
|
||||
|
|
@ -278,10 +294,9 @@ jobs:
|
|||
FROM: ${{ needs.gate.outputs.from }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# no apt: the installers were scanned with the private terms in their own jobs; this job only records
|
||||
- name: Tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends squashfs-tools p7zip-full xz-utils binutils unzip file >/dev/null
|
||||
curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" >/dev/null
|
||||
echo "$HOME/.bun/bin" >> "${FORGEJO_PATH:-$GITHUB_PATH}"
|
||||
[ -n "$FORGEJO_TOKEN" ] || { echo "secret RELEASE_TOKEN is missing (repo Settings > Actions > Secrets)" >&2; exit 1; }
|
||||
|
|
@ -308,15 +323,8 @@ jobs:
|
|||
ls cand/acceptance
|
||||
V=$(bun tools/desktop-version.ts --print)
|
||||
bun tools/acceptance.ts report cand/acceptance --md "cand/storebound_${V}_acceptance.md" --expect 6 --strict
|
||||
- name: Privacy scan (private terms) of the installers
|
||||
env:
|
||||
FORBIDDEN_TERMS: ${{ secrets.FORBIDDEN_TERMS }}
|
||||
run: |
|
||||
[ -n "$FORBIDDEN_TERMS" ] || { echo "secret FORBIDDEN_TERMS is missing" >&2; exit 1; }
|
||||
umask 077; printf '%s\n' "$FORBIDDEN_TERMS" > "$RUNNER_TEMP/terms.txt"
|
||||
rm -f cand/SHA256SUMS
|
||||
STOREBOUND_FORBIDDEN_TERMS="$RUNNER_TEMP/terms.txt" bun tools/privacy-scan.ts cand/*.AppImage cand/*.deb cand/*.exe
|
||||
rm -f "$RUNNER_TEMP/terms.txt"
|
||||
- name: Checksums file of the build jobs is not shipped as is
|
||||
run: rm -f cand/SHA256SUMS
|
||||
- name: Record the candidate (draft rc-<sha>, green.json last)
|
||||
run: |
|
||||
bun tools/release-candidate.ts record --sha "$SHA" --run "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" --dir cand ${FROM:+--reused-from "$FROM"} --yes
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
# Not here: macOS. tools/release.ts on the owner's Mac builds, signs and notarizes the DMG in parallel and adds it to
|
||||
# this release, then publishes storebound.dev (docs/desktop-release.md). The updater signing key never reaches a runner.
|
||||
# Secrets: RELEASE_TOKEN (scope write:repository), FORGEJO_ORIGIN (the server's IP: uploads over 100 MB bypass
|
||||
# Cloudflare). The private terms scan of the installers ran in ci.yml's record job (FORBIDDEN_TERMS).
|
||||
# Cloudflare). The private terms scan of the installers ran in ci.yml's build jobs (FORBIDDEN_TERMS).
|
||||
name: release
|
||||
|
||||
on:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ tested twice: every push to main is already a release candidate.
|
|||
cached, so only changed crates compile);
|
||||
- `windows`: the NSIS installer, silent per-user install, self-test, uninstall, the Node prompt smoke, privacy scan.
|
||||
|
||||
When all are green, the `record` job scans the installers with the private terms (secret `FORBIDDEN_TERMS`), builds
|
||||
The build jobs scan the installers with the private terms (secret `FORBIDDEN_TERMS`). When all are green, the `record` job builds
|
||||
the 6-run acceptance matrix, and stores everything as the **draft release `rc-<full sha>`** on git.crt.fyi:
|
||||
installers, `SHA256SUMS`, the acceptance run files, `storebound_<version>_acceptance.md` and `green.json` (sha,
|
||||
version, CLI key, run link, every file's SHA-256). `green.json` is uploaded last: a candidate without it is not
|
||||
|
|
|
|||
|
|
@ -101,7 +101,10 @@ export function mergeSums(existing: string, add: { file: string; sha256: string
|
|||
return [...m].sort((a, b) => a[0].localeCompare(b[0])).map(([f, h]) => `${h} ${f}`).join('\n') + '\n';
|
||||
}
|
||||
|
||||
export interface Release { id: number; tag_name: string; name: string; draft: boolean; prerelease: boolean; body: string; assets: { id: number; name: string; size: number; browser_download_url: string }[] }
|
||||
export interface Release { id: number; tag_name: string; name: string; draft: boolean; prerelease: boolean; body: string; assets: { id: number; name: string; size: number; uuid?: string; browser_download_url: string }[] }
|
||||
|
||||
/** Where an asset downloads: drafts (release candidates) only by their attachment uuid; the tag URL is 404 there. */
|
||||
export const assetUrl = (a: { uuid?: string; browser_download_url: string }): string => (a.uuid ? `${(process.env.FORGEJO_URL ?? "https://git.crt.fyi").replace(/\/+$/, "")}/attachments/${a.uuid}` : a.browser_download_url);
|
||||
|
||||
/** Release entries `prune --keep <tag>` deletes: every other entry, the kept one must exist and be published.
|
||||
* Release candidates (rc-<sha> drafts of ci.yml, tools/release-candidate.ts) are not entries: they have their own
|
||||
|
|
@ -241,7 +244,7 @@ export async function upsert(o: UpsertOptions): Promise<Release | null> {
|
|||
const files = o.files.filter(f => basename(f) !== SUMS);
|
||||
let rel = (await listReleases()).find(r => r.tag_name === o.tag) ?? null;
|
||||
const sumsBefore = rel?.assets.find(a => a.name === SUMS);
|
||||
const existingSums = sumsBefore ? await api<string>('GET', sumsBefore.browser_download_url, undefined, true) : '';
|
||||
const existingSums = sumsBefore ? await api<string>('GET', assetUrl(sumsBefore), undefined, true) : '';
|
||||
const sums = mergeSums(existingSums, files.map(f => ({ file: basename(f), sha256: sha256(f) })));
|
||||
console.log(`${rel ? `update release ${rel.id}` : 'create release'} ${o.tag}${o.draft ? ' (draft)' : ''}: ${o.title ?? rel?.name ?? o.tag}`);
|
||||
for (const f of files) console.log(` ${rel?.assets.some(a => a.name === basename(f)) ? 'replace' : 'attach '} ${basename(f)} (${(statSync(f).size / 1e6).toFixed(1)} MB)`);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSyn
|
|||
import { basename, join, resolve } from 'node:path';
|
||||
import { cliKey } from './cli-key';
|
||||
import { COPIES, SEMVER } from './desktop-version';
|
||||
import { api, BASE, candidateTag, deleteTest, downloadAsset, isCandidateTag, listReleases, type Release, REPO, shortNotes, SUMS, upsert } from './forgejo-release';
|
||||
import { api, assetUrl, BASE, candidateTag, deleteTest, downloadAsset, isCandidateTag, listReleases, type Release, REPO, shortNotes, SUMS, upsert } from './forgejo-release';
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..');
|
||||
export const GREEN = 'green.json';
|
||||
|
|
@ -147,7 +147,7 @@ async function tasks(): Promise<Task[]> {
|
|||
async function record(release: Release): Promise<GreenRecord | null> {
|
||||
const a = release.assets.find(x => x.name === GREEN);
|
||||
if (!a) return null;
|
||||
return JSON.parse(await api<string>('GET', a.browser_download_url, undefined, true)) as GreenRecord;
|
||||
return JSON.parse(await api<string>('GET', assetUrl(a), undefined, true)) as GreenRecord;
|
||||
}
|
||||
|
||||
export async function status(sha: string): Promise<Status> {
|
||||
|
|
@ -180,7 +180,7 @@ export async function fetchCandidate(s: Extract<Status, { state: 'green' }>, dir
|
|||
await Promise.all(assets.map(async a => {
|
||||
const dest = join(dir, a.name);
|
||||
if (existsSync(dest) && s.record.files[a.name] && sha256(dest) === s.record.files[a.name]) { got.push(dest); return; }
|
||||
await downloadAsset(a.browser_download_url, dest);
|
||||
await downloadAsset(assetUrl(a), dest);
|
||||
const want = s.record.files[a.name];
|
||||
if (want && sha256(dest) !== want) fail(`${a.name}: SHA-256 differs from the candidate's record`);
|
||||
got.push(dest);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue