|
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, |
@@ -243,45 +244,82 @@ describe.skipIf(!BASH_HAS_MAPFILE)( |
243 | 244 | * `build` reports the tree it tested and `publish` refuses any other, so the |
244 | 245 | * two must stage the guests identically. Cargo writes them executable and |
245 | 246 | * `publish` writes them 0644 from the artifact, which made the trees differ |
246 | | - * in mode alone: the first release refused itself. This runs the build job's |
247 | | - * staging, up to the tree it reports, on executable guests. |
| 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. |
248 | 249 | */ |
249 | 250 | const STAGE_STEP = WORKFLOW.jobs.build.steps.find((step) => step.id === 'stage') |
250 | 251 | const STAGE_UNTIL = 'fetched=$(mktemp -d)' |
251 | 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 | + |
252 | 291 | describe('release-golang.yml build: Stage the release commit', () => { |
253 | | - it('reports the tree publish builds, whatever mode cargo left the guests in', () => { |
| 292 | + it('commits the guests non-executable, whatever mode cargo left them in', () => { |
254 | 293 | expect(STAGE_STEP).toBeDefined() |
255 | | - const script = String(STAGE_STEP.run) |
256 | | - const cut = script.indexOf(STAGE_UNTIL) |
257 | | - expect( |
258 | | - cut, |
259 | | - `the stage step no longer contains ${STAGE_UNTIL}`, |
260 | | - ).toBeGreaterThan(0) |
261 | | - |
| 294 | + const tree = runBuildStaging() |
262 | 295 | for (const guest of GUESTS) { |
263 | | - stage(guest, `guest ${guest}\n`) |
264 | | - const built = join(repo, guest) |
265 | | - mkdirSync(dirname(built), { recursive: true }) |
266 | | - writeFileSync(built, `guest ${guest}\n`, { mode: 0o755 }) |
| 296 | + expect(git(repo, 'ls-tree', tree, guest).split(/\s+/)[0], guest).toBe( |
| 297 | + '100644', |
| 298 | + ) |
267 | 299 | } |
268 | | - const result = spawnSync( |
269 | | - 'bash', |
270 | | - ['-c', `${script.slice(0, cut)}\necho "$tree"`], |
271 | | - { |
272 | | - cwd: repo, |
273 | | - encoding: 'utf8', |
274 | | - env: { |
275 | | - PATH: process.env.PATH, |
276 | | - HOME: repo, |
277 | | - GIT_CONFIG_NOSYSTEM: '1', |
278 | | - GUESTS: WORKFLOW.env.GUESTS, |
279 | | - VERSION: '0.1.0-alpha.1', |
280 | | - }, |
281 | | - }, |
282 | | - ) |
283 | | - expect(result.status, result.stderr).toBe(0) |
284 | | - git(repo, 'checkout', '-q', 'main') |
285 | | - expect(result.stdout.trim().split('\n').at(-1)).toBe(expectedTree()) |
286 | 300 | }) |
287 | 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