diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index c1ae68934..46c0b6d7d 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -205,7 +205,7 @@ PR_BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/gstack-pr-body.XXXXXX") || { echo "ERROR: BODY_TOP_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" BODY_REST_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" [ -s "$BODY_TOP_FILE" ] && [ -s "$BODY_REST_FILE" ] || { echo "Not composed: $BODY_TOP_FILE or $BODY_REST_FILE is empty, so the body was never written. Write both, then rerun." >&2; exit 1; } -~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), then rerun." >&2; exit 1; } +~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate >&2 || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), then rerun." >&2; exit 1; } { awk 1 "$BODY_TOP_FILE" && cat -- "$DOCS_SECTION_FILE" && echo && awk 1 "$BODY_REST_FILE"; } > "$PR_BODY_FILE" || exit 1 rm -f "$BODY_TOP_FILE" "$BODY_REST_FILE" echo "PR_BODY_FILE: $PR_BODY_FILE" diff --git a/ship/sections/pr-body.md.tmpl b/ship/sections/pr-body.md.tmpl index e5ca8a9bc..ffdcc4eb2 100644 --- a/ship/sections/pr-body.md.tmpl +++ b/ship/sections/pr-body.md.tmpl @@ -188,7 +188,7 @@ PR_BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/gstack-pr-body.XXXXXX") || { echo "ERROR: BODY_TOP_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" BODY_REST_FILE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/" [ -s "$BODY_TOP_FILE" ] && [ -s "$BODY_REST_FILE" ] || { echo "Not composed: $BODY_TOP_FILE or $BODY_REST_FILE is empty, so the body was never written. Write both, then rerun." >&2; exit 1; } -~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), then rerun." >&2; exit 1; } +~/.claude/skills/gstack/bin/gstack-prose-lint "$BODY_TOP_FILE" --sections 'Why,Summary,Blast radius' --gate >&2 || { echo "Not composed: fix what gstack-prose-lint printed above (a PROSE: line is a sentence to rewrite in the draft; a usage line means the draft lost a heading), then rerun." >&2; exit 1; } { awk 1 "$BODY_TOP_FILE" && cat -- "$DOCS_SECTION_FILE" && echo && awk 1 "$BODY_REST_FILE"; } > "$PR_BODY_FILE" || exit 1 rm -f "$BODY_TOP_FILE" "$BODY_REST_FILE" echo "PR_BODY_FILE: $PR_BODY_FILE" diff --git a/test/ship-publication-gates.test.ts b/test/ship-publication-gates.test.ts index f5ffd111f..ab0663b72 100644 --- a/test/ship-publication-gates.test.ts +++ b/test/ship-publication-gates.test.ts @@ -14,6 +14,16 @@ function writeDrafts(root: string, top = '## Summary\n\n## Documentation', rest return block => block.replace('', 'pr-body-top.abc123').replace('', 'pr-body-rest.abc123'); } const POST = resolve(import.meta.dir, '../bin/gstack-post'); +const PROSE_LINT = resolve(import.meta.dir, '../bin/gstack-prose-lint'); + +/** The two real bins the blocks call, at the installed path under a HOME of `root`. */ +function installBins(root: string): void { + const bin = join(root, '.claude/skills/gstack/bin'); + mkdirSync(bin, { recursive: true }); + for (const [name, target] of [['gstack-post', POST], ['gstack-prose-lint', PROSE_LINT]] as const) { + writeFileSync(join(bin, name), `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(target)} "$@"\n`, { mode: 0o755 }); + } +} const fenceAfter = (marker: string) => { const at = template.indexOf(marker); expect(at).toBeGreaterThan(-1); @@ -26,7 +36,7 @@ const createBlock = fenceAfter('**No open PR/MR:**'); /** * A repo whose origin is GitHub, a stub gh that records argv and stdin, and - * the real gstack-post at the installed path the blocks call. + * the real gstack-post and gstack-prose-lint at the installed path the blocks call. */ function publicationRepo(root: string) { const log = join(root, 'log'); @@ -38,7 +48,7 @@ function publicationRepo(root: string) { for (const args of [['init', '-q'], ['remote', 'add', 'origin', 'https://github.com/o/r.git']]) { Bun.spawnSync(['git', ...args], { cwd: root, env: { ...process.env, ...gitEnv }, timeout: 10_000 }); } - writeFileSync(join(bin, 'gstack-post'), `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(POST)} "$@"\n`, { mode: 0o755 }); + installBins(root); writeFileSync(join(stubs, 'gh'), `#!/usr/bin/env bash case "$1 $2" in "repo view") echo PRIVATE; exit 0 ;; @@ -133,6 +143,7 @@ test('publication composes the saved documentation section unchanged and refuses const compose = block.slice(block.indexOf('PR_BODY_FILE=$(mktemp'), block.indexOf('echo "PR_BODY_FILE:')); const root = mkdtempSync(join(tmpdir(), 'ship-compose-')); try { + installBins(root); const section = '**Status:** current — no edits.\n\n- Diagram drift: none (no diagrams in any doc).'; writeFileSync(join(root, 'section.md'), section); const run = (file: string) => Bun.spawnSync(['bash', '-c', `${writeDrafts(root)(compose)}cat -- "$PR_BODY_FILE"`], {