diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a82dbc0fe..9f9d9adab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -756,6 +756,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: @@ -763,6 +771,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 639c75057..0ffaff9dd 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,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..676e5ad80 --- /dev/null +++ b/scripts/ci-release-checksums.ts @@ -0,0 +1,54 @@ +#!/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 hasher = new Bun.CryptoHasher("sha256"); + for await (const chunk of Bun.file(assetPath).stream()) { + hasher.update(chunk); + } + return { name: path.basename(assetPath), sha256: hasher.digest("hex") }; + }), + ); + + await Bun.write(outFile, formatChecksums(entries)); + console.log(`Wrote ${entries.length} checksum(s) to ${outFile}`); +} + +if (import.meta.main) { + await main(); +}