diff --git a/.github/workflows/release-golang.yml b/.github/workflows/release-golang.yml index 59a325f08..0db79e34a 100644 --- a/.github/workflows/release-golang.yml +++ b/.github/workflows/release-golang.yml @@ -205,7 +205,11 @@ jobs: git checkout --detach # The guests are gitignored on main; -f adds them to this commit only. read -ra guests <<< "$GUESTS" - git add -f "${guests[@]}" + # --chmod=-x: the committed mode is set in git, not read from the + # file. Cargo writes the guests executable and `publish` writes them + # 0644, so a mode taken from disk made this job's tree differ from + # the one `publish` builds, and it refused the release. + git add -f --chmod=-x "${guests[@]}" git commit -q -m "release(golang): v$VERSION with its WASI guests" tree=$(git rev-parse 'HEAD^{tree}') @@ -298,7 +302,9 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout --detach - git add -f "${guests[@]}" + # The mode is set in git, as the build job sets it, so the two trees + # agree whatever mode either job left the files in. + git add -f --chmod=-x "${guests[@]}" git commit -m "release(golang): v$VERSION with its WASI guests" \ -m "Built from $GITHUB_SHA by $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" tree=$(git rev-parse 'HEAD^{tree}') diff --git a/scripts/__tests__/go-release-artifact.test.mjs b/scripts/__tests__/go-release-artifact.test.mjs index 6129de716..8d4a217ab 100644 --- a/scripts/__tests__/go-release-artifact.test.mjs +++ b/scripts/__tests__/go-release-artifact.test.mjs @@ -1,6 +1,7 @@ import { execFileSync, spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { + chmodSync, existsSync, mkdirSync, mkdtempSync, @@ -238,3 +239,87 @@ describe.skipIf(!BASH_HAS_MAPFILE)( }) }, ) + +/** + * `build` reports the tree it tested and `publish` refuses any other, so the + * two must stage the guests identically. Cargo writes them executable and + * `publish` writes them 0644 from the artifact, which made the trees differ + * in mode alone: the first release refused itself. Both now set the mode in + * git (`--chmod=-x`), so it no longer depends on the files. + */ +const STAGE_STEP = WORKFLOW.jobs.build.steps.find((step) => step.id === 'stage') +const STAGE_UNTIL = 'fetched=$(mktemp -d)' + +/** + * Run the build job's staging, up to the tree it reports, on guests left + * executable as cargo leaves them, then return to `main`. Returns the tree. + */ +function runBuildStaging() { + const script = String(STAGE_STEP.run) + const cut = script.indexOf(STAGE_UNTIL) + expect( + cut, + `the stage step no longer contains ${STAGE_UNTIL}`, + ).toBeGreaterThan(0) + for (const guest of GUESTS) { + const built = join(repo, guest) + mkdirSync(dirname(built), { recursive: true }) + writeFileSync(built, `guest ${guest}\n`, { mode: 0o755 }) + } + const result = spawnSync( + 'bash', + ['-c', `${script.slice(0, cut)}\necho "$tree"`], + { + cwd: repo, + encoding: 'utf8', + env: { + PATH: process.env.PATH, + HOME: repo, + GIT_CONFIG_NOSYSTEM: '1', + GUESTS: WORKFLOW.env.GUESTS, + VERSION: '0.1.0-alpha.1', + }, + }, + ) + expect(result.status, result.stderr).toBe(0) + // Forced: the guests on disk are still 0755 while the commit holds them + // 0644, which git reads as a local change. + git(repo, 'checkout', '-q', '-f', 'main') + return result.stdout.trim().split('\n').at(-1) +} + +describe('release-golang.yml build: Stage the release commit', () => { + it('commits the guests non-executable, whatever mode cargo left them in', () => { + expect(STAGE_STEP).toBeDefined() + const tree = runBuildStaging() + for (const guest of GUESTS) { + expect(git(repo, 'ls-tree', tree, guest).split(/\s+/)[0], guest).toBe( + '100644', + ) + } + }) +}) + +describe.skipIf(!BASH_HAS_MAPFILE)( + 'release-golang.yml build and publish build the same tree', + () => { + it.skipIf(!GNU_COREUTILS)( + 'publish accepts the tree build reports, from an executable artifact', + () => { + const tree = runBuildStaging() + // The artifact executable too: the two jobs agree through git, not + // through what either one wrote to disk. + writeManifest( + GUESTS.map((guest) => { + const line = stage(guest, `guest ${guest}\n`) + chmodSync(join(repo, 'release-out', guest), 0o755) + return line + }), + ) + const result = runCommitStep(tree) + expect(result.stderr + result.stdout).not.toContain('::error::') + expect(result.status).toBe(0) + }, + ) + }, +)