diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7b09ef299..db67ef058f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: # both paths must trigger the sdk job. if echo "$files" | grep -qE '^(sdk/|internal/ext/extension/)'; then sdk=true; fi # The release-note tests render every committed Studio note; npm/ holds the publish tests. - if echo "$files" | grep -qE '^(scripts/|npm/|release-notes/studio/)'; then scripts=true; fi + if echo "$files" | grep -qE '^(scripts/|npm/|release-notes/studio/|\.github/workflows/release-studio\.yml$)'; then scripts=true; fi fi fi { echo "code=$code"; echo "desktop=$desktop"; echo "sdk=$sdk"; echo "scripts=$scripts"; } >> "$GITHUB_OUTPUT" @@ -80,7 +80,11 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "22" - - run: node --test scripts/release-credits.test.mjs scripts/update-contributors.test.mjs scripts/markdown-refs.test.mjs scripts/studio-release-notes.test.mjs scripts/publish-cli-pointer.test.mjs scripts/feedback-sync.test.mjs npm/publish.test.mjs + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 + with: + go-version-file: go.mod + cache: true + - run: node --test scripts/cli-archive-build.test.mjs scripts/release-credits.test.mjs scripts/update-contributors.test.mjs scripts/markdown-refs.test.mjs scripts/studio-release-notes.test.mjs scripts/publish-cli-pointer.test.mjs scripts/feedback-sync.test.mjs npm/publish.test.mjs # The ruleset requires the per-OS check names (test (ubuntu-latest) etc.). # A matrix job skipped at job level reports no per-leg checks at all, so diff --git a/.github/workflows/release-studio.yml b/.github/workflows/release-studio.yml index c04ecf5bbc..703056d524 100644 --- a/.github/workflows/release-studio.yml +++ b/.github/workflows/release-studio.yml @@ -757,14 +757,18 @@ jobs: - name: Cross-compile and archive env: VERSION: ${{ needs.resolve.outputs.version }} + SHA: ${{ needs.resolve.outputs.sha }} run: | set -euo pipefail mkdir -p dist + built="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + pkg=reasonix/internal/tools/productdocs for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do os="${target%/*}"; arch="${target#*/}"; ext="" [ "$os" = windows ] && ext=".exe" CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -trimpath \ - -ldflags "-s -w -X main.version=$VERSION" -o "reasonix$ext" ./cmd/reasonix + -ldflags "-s -w -X main.version=$VERSION -X main.gitCommit=${SHA:0:12} -X main.buildTimeUTC=$built -X $pkg.linkedVersion=$VERSION -X $pkg.linkedRevision=$SHA" \ + -o "reasonix$ext" ./cmd/reasonix if [ "$os" = windows ]; then zip -qj "dist/reasonix-$os-$arch.zip" "reasonix$ext" else diff --git a/scripts/cli-archive-build.test.mjs b/scripts/cli-archive-build.test.mjs new file mode 100644 index 0000000000..f62222fbd7 --- /dev/null +++ b/scripts/cli-archive-build.test.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(here, ".."); +const lines = readFileSync(path.join(root, ".github/workflows/release-studio.yml"), "utf8").split("\n"); + +function stepRun(name) { + const at = lines.findIndex((l) => l.trim() === `- name: ${name}`); + assert.ok(at >= 0, `no step named ${name}`); + const run = lines.findIndex((l, i) => i > at && l.trim() === "run: |"); + const indent = lines[run].search(/\S/) + 2; + const body = []; + for (let i = run + 1; i < lines.length; i += 1) { + if (lines[i].trim() !== "" && lines[i].search(/\S/) < indent) break; + body.push(lines[i].slice(indent)); + } + return body.join("\n"); +} + +test("the archives the updaters install report the commit and release they were built from", () => { + const out = mkdtempSync(path.join(tmpdir(), "cli-archive-")); + try { + const goenv = (key) => execFileSync("go", ["env", key], { encoding: "utf8" }).trim(); + const [goos, goarch] = [goenv("GOOS"), goenv("GOARCH")]; + const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim(); + const rewrite = (text, from, to) => { + const next = text.replace(from, to); + assert.notEqual(next, text, `the step no longer contains ${from}; update this test`); + return next; + }; + let script = stepRun("Cross-compile and archive"); + script = rewrite(script, /for target in [^;]+; do/, `for target in ${goos}/${goarch}; do`); + script = rewrite(script, /^\(cd .* sha256sum .*$/m, ""); + script = script.replaceAll("dist", `"${out}"`); + execFileSync("bash", ["-c", script], { + cwd: root, + env: { ...process.env, VERSION: "v9.8.7", SHA: sha }, + stdio: "pipe", + }); + const archive = path.join(out, `reasonix-${goos}-${goarch}.${goos === "windows" ? "zip" : "tar.gz"}`); + execFileSync("tar", ["xf", archive, "-C", out]); + const exe = path.join(out, goos === "windows" ? "reasonix.exe" : "reasonix"); + const report = execFileSync(exe, ["version", "--verbose"], { encoding: "utf8" }); + assert.match(report, /^reasonix v9\.8\.7$/m); + assert.ok(report.includes(`git_commit: ${sha.slice(0, 12)}`), report); + assert.match(report, /^build_time_utc: \d{4}-\d\d-\d\dT/m); + const binary = readFileSync(exe); + assert.ok(binary.includes(sha), "the docs corpus revision is not linked"); + } finally { + rmSync(out, { recursive: true, force: true }); + } +});