|
1 | 1 | import { execFileSync, spawnSync } from 'node:child_process' |
2 | 2 | import { createHash } from 'node:crypto' |
3 | 3 | import { |
| 4 | + chmodSync, |
4 | 5 | existsSync, |
5 | 6 | mkdirSync, |
6 | 7 | mkdtempSync, |
@@ -238,3 +239,87 @@ describe.skipIf(!BASH_HAS_MAPFILE)( |
238 | 239 | }) |
239 | 240 | }, |
240 | 241 | ) |
| 242 | + |
| 243 | +/** |
| 244 | + * `build` reports the tree it tested and `publish` refuses any other, so the |
| 245 | + * two must stage the guests identically. Cargo writes them executable and |
| 246 | + * `publish` writes them 0644 from the artifact, which made the trees differ |
| 247 | + * in mode alone: the first release refused itself. Both now set the mode in |
| 248 | + * git (`--chmod=-x`), so it no longer depends on the files. |
| 249 | + */ |
| 250 | +const STAGE_STEP = WORKFLOW.jobs.build.steps.find((step) => step.id === 'stage') |
| 251 | +const STAGE_UNTIL = 'fetched=$(mktemp -d)' |
| 252 | + |
| 253 | +/** |
| 254 | + * Run the build job's staging, up to the tree it reports, on guests left |
| 255 | + * executable as cargo leaves them, then return to `main`. Returns the tree. |
| 256 | + */ |
| 257 | +function runBuildStaging() { |
| 258 | + const script = String(STAGE_STEP.run) |
| 259 | + const cut = script.indexOf(STAGE_UNTIL) |
| 260 | + expect( |
| 261 | + cut, |
| 262 | + `the stage step no longer contains ${STAGE_UNTIL}`, |
| 263 | + ).toBeGreaterThan(0) |
| 264 | + for (const guest of GUESTS) { |
| 265 | + const built = join(repo, guest) |
| 266 | + mkdirSync(dirname(built), { recursive: true }) |
| 267 | + writeFileSync(built, `guest ${guest}\n`, { mode: 0o755 }) |
| 268 | + } |
| 269 | + const result = spawnSync( |
| 270 | + 'bash', |
| 271 | + ['-c', `${script.slice(0, cut)}\necho "$tree"`], |
| 272 | + { |
| 273 | + cwd: repo, |
| 274 | + encoding: 'utf8', |
| 275 | + env: { |
| 276 | + PATH: process.env.PATH, |
| 277 | + HOME: repo, |
| 278 | + GIT_CONFIG_NOSYSTEM: '1', |
| 279 | + GUESTS: WORKFLOW.env.GUESTS, |
| 280 | + VERSION: '0.1.0-alpha.1', |
| 281 | + }, |
| 282 | + }, |
| 283 | + ) |
| 284 | + expect(result.status, result.stderr).toBe(0) |
| 285 | + // Forced: the guests on disk are still 0755 while the commit holds them |
| 286 | + // 0644, which git reads as a local change. |
| 287 | + git(repo, 'checkout', '-q', '-f', 'main') |
| 288 | + return result.stdout.trim().split('\n').at(-1) |
| 289 | +} |
| 290 | + |
| 291 | +describe('release-golang.yml build: Stage the release commit', () => { |
| 292 | + it('commits the guests non-executable, whatever mode cargo left them in', () => { |
| 293 | + expect(STAGE_STEP).toBeDefined() |
| 294 | + const tree = runBuildStaging() |
| 295 | + for (const guest of GUESTS) { |
| 296 | + expect(git(repo, 'ls-tree', tree, guest).split(/\s+/)[0], guest).toBe( |
| 297 | + '100644', |
| 298 | + ) |
| 299 | + } |
| 300 | + }) |
| 301 | +}) |
| 302 | + |
| 303 | +describe.skipIf(!BASH_HAS_MAPFILE)( |
| 304 | + 'release-golang.yml build and publish build the same tree', |
| 305 | + () => { |
| 306 | + it.skipIf(!GNU_COREUTILS)( |
| 307 | + 'publish accepts the tree build reports, from an executable artifact', |
| 308 | + () => { |
| 309 | + const tree = runBuildStaging() |
| 310 | + // The artifact executable too: the two jobs agree through git, not |
| 311 | + // through what either one wrote to disk. |
| 312 | + writeManifest( |
| 313 | + GUESTS.map((guest) => { |
| 314 | + const line = stage(guest, `guest ${guest}\n`) |
| 315 | + chmodSync(join(repo, 'release-out', guest), 0o755) |
| 316 | + return line |
| 317 | + }), |
| 318 | + ) |
| 319 | + const result = runCommitStep(tree) |
| 320 | + expect(result.stderr + result.stdout).not.toContain('::error::') |
| 321 | + expect(result.status).toBe(0) |
| 322 | + }, |
| 323 | + ) |
| 324 | + }, |
| 325 | +) |
0 commit comments