fix(#1520): randomize mktemp temp paths on BSD/macOS (XXXXXX must be path-final) (#1550)

* fix(#1520): randomize mktemp temp paths on BSD/macOS (XXXXXX must be path-final)

BSD/macOS mktemp only substitutes the XXXXXX template when it is the final
path component. Templates like `...-XXXXXX.json` / `gsd-pr-body.XXXXXX.md`
return a LITERAL `XXXXXX` path (no randomization) on macOS, so concurrent
workflow runs collide on the same temp manifest/body file — one run can
overwrite or consume another's. Reproduced on macOS: the second call to the
suffixed template fails `mkstemp: File exists`.

Fix: use a suffixless `XXXXXX` template (so it IS the final component), then
rename to add the intended extension — portable across BSD + GNU userlands,
no GNU-only `--suffix` flag. Empty-file-then-write semantics are preserved at
every site.

Affected workflow temp files:
- execute-phase.md: gsd-worktree-wave-*.json (wave worktree manifest)
- quick.md:         gsd-quick-worktree-*.json
- spec-phase.md:    edge-probe-reqs-*.json
- ship.md:          gsd-pr-body-*.md
- profile-user.md:  gsd-profile-answers-*.json, gsd-profile-analysis-*.json

The execute-phase.md edit uses a compact intermediate var + trailing comment
to stay under the ADR-857 phase-6 size ceiling (93166); regenerated the
workflow size baseline accordingly. Validated on macOS: 20 concurrent calls
yield 20 unique randomized paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1520): add changeset fragment (Fixed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#1520): add fail-first workflow-prose guard for mktemp XXXXXX suffix

Repo-wide scan of gsd-core/workflows/**/*.md that fails on any mktemp
template whose XXXXXX run is followed by a filename suffix (the BSD/macOS
non-randomizing form). Fails on the six pre-fix instances and passes on
the fix, and locks the copy-paste-prone idiom out of future workflows.
Mirrors the bug-637 hardcoded-$HOME workflow guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#1520): rename regression test to fix- prefix (regression-test-names lint)

New tests/bug-NNNN-*.test.cjs files are banned by the lint-regression-test-names
ratchet; use the fix- prefix (matches the fix-1445 precedent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#1520): add issue ref to allow-test-rule exemption (ADR-456 lint)

lint-allow-test-rule-refs requires every new `allow-test-rule:` comment to
carry a #NNN reference (don't allowlist). Add (#1520) to the source-text
exemption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#1520): abort touched mktemp chains on failure (|| exit 1)

Per review: the VAR=$(mktemp …) && mv … && VAR=… chains dropped the issue's
suggested failure guard. If mktemp fails, $VAR is empty and the subsequent
mv/write lands on an unintended relative path. Add `|| exit 1` to all six
touched chains so a mktemp failure aborts the snippet. Regenerated the
workflow size baseline for the slightly longer lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1520): rebase onto next — regen size baseline + describe rename

Resolve the workflow-size-baseline.json conflict from next advancing by
regenerating from the current workflow sizes. Also rename the test describe
from `bug #1520` to `#1520` (the file uses the fix- prefix) per review nit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#1520): harmonize profile-user mktemp to ${TMPDIR:-/tmp} (review nit)

The two profile-user.md temp sites this PR already rewrites kept a hardcoded
/tmp while the four sibling workflows use ${TMPDIR:-/tmp}. Harmonize for
consistency and macOS-correctness (some sandboxes have no writable /tmp).
Regenerated the workflow size baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1520): regen size baseline after rebase onto next

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Behruz Nassre Esfahani
2026-06-24 14:27:45 -07:00
committed by GitHub
parent cbf7c82841
commit 47906b052d
8 changed files with 104 additions and 11 deletions

View File

@@ -0,0 +1,78 @@
// allow-test-rule: source-text-is-the-product (#1520)
// Workflow .md text IS what the runtime loads and the agent executes, so
// asserting on its shell invocations tests the deployed contract directly.
//
// Repo-wide regression guard for #1520: NO workflow .md may invoke `mktemp`
// with a template whose `XXXXXX` run is followed by a filename suffix
// (e.g. `…-XXXXXX.json`, `…-XXXXXX.md`). BSD/macOS `mktemp` only substitutes
// the `X` run when it is the FINAL path component; a trailing suffix yields a
// literal, non-randomized path, so concurrent workflow runs collide on the same
// temp file (one run overwriting or consuming another's). The portable fix is
// `mktemp …-XXXXXX` (suffix-less) then `mv` to add the extension.
//
// This is a copy-paste-prone shell idiom — the same defect first shipped across
// five workflows before #1520 — so a prose guard is the right lock-out, mirroring
// the bug-637 hardcoded-$HOME workflow scan.
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
// Match `mktemp <token>` where, within the single whitespace-delimited template
// token, a maximal run of 3+ `X` is immediately followed by a filename
// character (`.`, alnum, `-`, `_`) — i.e. a suffix the BSD/macOS substitution
// can't reach.
// - `\s+` requires an argument (bare `mktemp` is fine — path-final
// is N/A — and prose like "mktemp only randomizes XXXXXX"
// is excluded because the X-run is in a later token).
// - `["']?\S*?` walks within the one quoted/unquoted template token.
// - `X{3,}(?!X)` anchors on the WHOLE X-run (so `XXXXXX)` does not match
// via a sub-run leaving a trailing `X`).
// - `[.A-Za-z0-9_-]` the offending suffix char. A legitimate path-final form
// ends the token with `"`, `'`, whitespace, or `)`, none of
// which are in this class.
const SUFFIXED_MKTEMP_TEMPLATE = /mktemp\s+["']?\S*?X{3,}(?!X)[.A-Za-z0-9_-]/;
function collectWorkflowMarkdown(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...collectWorkflowMarkdown(full));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
out.push(full);
}
}
return out;
}
describe('#1520: workflow mktemp templates keep XXXXXX path-final', () => {
test('no gsd-core/workflows/**/*.md calls mktemp with a suffix after the XXXXXX run', () => {
const files = collectWorkflowMarkdown(WORKFLOWS_DIR);
assert.ok(files.length > 0, 'expected workflow markdown files to exist');
const offenders = [];
for (const file of files) {
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
lines.forEach((line, i) => {
if (SUFFIXED_MKTEMP_TEMPLATE.test(line)) {
offenders.push(`${path.relative(WORKFLOWS_DIR, file)}:${i + 1}: ${line.trim()}`);
}
});
}
assert.deepStrictEqual(
offenders,
[],
'Workflow mktemp templates must keep XXXXXX as the final path component ' +
'(create suffix-less, then `mv` to add the extension) so BSD/macOS ' +
'randomizes the path. Offenders:\n' +
offenders.join('\n'),
);
});
});

View File

@@ -24,7 +24,7 @@
"docs-update.md": 55662,
"edit-phase.md": 12883,
"eval-review.md": 9923,
"execute-phase.md": 93426,
"execute-phase.md": 93517,
"execute-plan.md": 32611,
"explore.md": 10497,
"extract-learnings.md": 12849,
@@ -56,9 +56,9 @@
"plan-review-convergence.md": 23468,
"plant-seed.md": 11741,
"pr-branch.md": 15919,
"profile-user.md": 20650,
"profile-user.md": 21202,
"progress.md": 30555,
"quick.md": 48830,
"quick.md": 49139,
"reapply-patches.md": 20393,
"remove-phase.md": 8469,
"remove-workspace.md": 7507,
@@ -70,10 +70,10 @@
"settings-advanced.md": 39666,
"settings-integrations.md": 15848,
"settings.md": 33413,
"ship.md": 24388,
"ship.md": 24647,
"sketch-wrap-up.md": 14223,
"sketch.md": 19960,
"spec-phase.md": 31503,
"spec-phase.md": 31752,
"spike-wrap-up.md": 15092,
"spike.md": 24517,
"stats.md": 6718,