From 5da9525afd6ed9ed9ad486491d6925bfd4dfe2f5 Mon Sep 17 00:00:00 2001 From: Andrew Fischer Date: Tue, 4 Aug 2026 15:38:18 -0700 Subject: [PATCH] ci(release): add SHA256SUMS.txt to GitHub releases Generate a sha256sum-compatible checksums file covering every release binary and the browser-relay zip, and upload it as a release asset so downloads can be verified offline without hitting the GitHub API. --- .github/workflows/ci.yml | 9 +++++ package.json | 1 + scripts/ci-release-checksums.test.ts | 16 +++++++++ scripts/ci-release-checksums.ts | 51 ++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 scripts/ci-release-checksums.test.ts create mode 100644 scripts/ci-release-checksums.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82e5e6a24..37be1b282 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -751,6 +751,14 @@ jobs: # is needed on this runner. - name: Build browser relay artifacts run: bun run --cwd packages/browser-relay build + # Generated after every other release asset is in place and before + # the release is created, so SHA256SUMS.txt itself ships as an asset + # and covers every other file uploaded alongside it. + - name: Generate checksums + run: | + bun run ci:release:checksums SHA256SUMS.txt \ + packages/coding-agent/binaries/omp-* \ + packages/browser-relay/dist/omp-browser-relay-extension.zip - name: Create GitHub Release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: @@ -758,6 +766,7 @@ jobs: files: | packages/coding-agent/binaries/omp-* packages/browser-relay/dist/omp-browser-relay-extension.zip + SHA256SUMS.txt body_path: release-notes.md generate_release_notes: true diff --git a/package.json b/package.json index e763563f7..1ddae78b3 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "ci:test:smoke": "bun packages/coding-agent/src/cli.ts --version && bun packages/coding-agent/src/cli.ts --help && bun packages/coding-agent/src/cli.ts stats --help && bun packages/coding-agent/src/cli.ts --smoke-test", "ci:test:install-methods": "bash scripts/install-tests/run-ci.sh", "ci:release:build-binaries": "bun scripts/ci-release-build-binaries.ts", + "ci:release:checksums": "bun scripts/ci-release-checksums.ts", "ci:release:publish": "bun scripts/ci-release-publish.ts", "ci:release:publish-native-leaf": "bun scripts/ci-release-publish.ts --native-leaf", "bench:gen-fixtures": "bun --cwd=packages/typescript-edit-benchmark run src/generate.ts --typescript-dir /tmp/typescript-source --count-per-type 8", diff --git a/scripts/ci-release-checksums.test.ts b/scripts/ci-release-checksums.test.ts new file mode 100644 index 000000000..78268c4b0 --- /dev/null +++ b/scripts/ci-release-checksums.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "bun:test"; +import { formatChecksums } from "./ci-release-checksums"; + +describe("formatChecksums", () => { + it("sorts entries by basename and formats as `sha256sum -c`-compatible lines", () => { + const output = formatChecksums([ + { name: "omp-linux-x64", sha256: "b".repeat(64) }, + { name: "omp-darwin-arm64", sha256: "a".repeat(64) }, + ]); + expect(output).toBe(`${"a".repeat(64)} omp-darwin-arm64\n${"b".repeat(64)} omp-linux-x64\n`); + }); + + it("returns an empty string for no entries", () => { + expect(formatChecksums([])).toBe(""); + }); +}); diff --git a/scripts/ci-release-checksums.ts b/scripts/ci-release-checksums.ts new file mode 100644 index 000000000..630357e03 --- /dev/null +++ b/scripts/ci-release-checksums.ts @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +/** + * Generate a `sha256sum`-compatible checksums file for release assets. + * + * Usage: + * bun scripts/ci-release-checksums.ts ... + * + * Each `` is hashed and written as a ` ` line, + * sorted by basename, so the result can be verified after download with + * `sha256sum -c SHA256SUMS.txt` (or `shasum -a 256 -c` on macOS). + * + * Intended for the `release_github` CI job, run after the release binaries + * and browser-relay archive are assembled and before the GitHub Release is + * created, so the checksums file itself ships as one of the release assets. + */ + +import * as path from "node:path"; + +export interface ChecksumEntry { + name: string; + sha256: string; +} + +export function formatChecksums(entries: readonly ChecksumEntry[]): string { + return entries + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(({ sha256, name }) => `${sha256} ${name}\n`) + .join(""); +} + +async function main(): Promise { + const [outFile, ...assetPaths] = process.argv.slice(2); + if (!outFile || assetPaths.length === 0) { + throw new Error("usage: ci-release-checksums.ts ..."); + } + + const entries = await Promise.all( + assetPaths.map(async assetPath => { + const bytes = await Bun.file(assetPath).bytes(); + return { name: path.basename(assetPath), sha256: Bun.SHA256.hash(bytes, "hex") }; + }), + ); + + await Bun.write(outFile, formatChecksums(entries)); + console.log(`Wrote ${entries.length} checksum(s) to ${outFile}`); +} + +if (import.meta.main) { + await main(); +}