From ea81917445ef92afb9c3570289d6639740c9b3fa Mon Sep 17 00:00:00 2001 From: CrewCoder Date: Fri, 4 Sep 2026 21:59:10 -0400 Subject: [PATCH] =?UTF-8?q?I=20fixed=20the=20release=20script=E2=80=99s=20?= =?UTF-8?q?terminal/job-control=20exposure.=20Its=20npm=20subprocesses=20n?= =?UTF-8?q?ow=20run=20with=20stdin=20disconnected=20and=20CI=20mode=20forc?= =?UTF-8?q?ed,=20while=20Git=20fetch/push=20remain=20interactive.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 ++++++ docs/releasing.md | 8 ++++++++ scripts/release.mjs | 8 +++++++- src/main/release-script.test.ts | 13 +++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/main/release-script.test.ts diff --git a/AGENTS.md b/AGENTS.md index 3054f54..da18382 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,12 @@ npm run ship -- "feat: msg" # Stage + commit + push current branch to origin npm run release # Verify, bump patch version, tag, push -> triggers CI release build ``` +Release npm verification/version subprocesses must remain non-interactive with stdin +disconnected and CI mode enabled so nested commands cannot suspend a release through +shell job control. Keep Git fetch/push on the separate interactive path for credential +helpers, and never infer release success until the version commit, tag, and push are +all observed. See `docs/releasing.md`. + > `npm run dev` uses `env -u ELECTRON_RUN_AS_NODE` to prevent Electron's Node.js mode from interfering. ## Architecture diff --git a/docs/releasing.md b/docs/releasing.md index 1f85b27..abf6ff4 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -33,6 +33,14 @@ The script refuses to proceed unless: Then it runs `typecheck` + `test`, bumps the version, tags `vX.Y.Z`, and pushes with `--follow-tags`. +The npm verification and version subprocesses run in explicit CI mode with stdin +disconnected. This keeps nested test and commit processes out of interactive shell +job control, preventing fish or another POSIX shell from suspending the release when +a child attempts to read from the terminal. Git fetch/push retain terminal access so +configured credential helpers can still authenticate. A verification command that +needs input must fail explicitly rather than leaving a stopped release job that could +later resume and mutate version/tag state. + ## What CI does Pushing a `v*` tag triggers `.github/workflows/release.yml`, which builds on diff --git a/scripts/release.mjs b/scripts/release.mjs index a847163..c8ba359 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -14,7 +14,13 @@ const capture = args => const git = args => execFileSync('git', args, { stdio: 'inherit' }) const npm = args => execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { - stdio: 'inherit', + // Release verification and `npm version` are deliberately non-interactive. + // Inheriting a shell PTY's stdin lets nested test/git processes participate + // in fish job control and can suspend the entire release with SIGTTIN. + // Git fetch/push keep their separate interactive path above for credential + // helpers that legitimately need the terminal. + stdio: ['ignore', 'inherit', 'inherit'], + env: { ...process.env, CI: '1' }, }) const bump = process.argv[2] ?? 'patch' diff --git a/src/main/release-script.test.ts b/src/main/release-script.test.ts new file mode 100644 index 0000000..e66956c --- /dev/null +++ b/src/main/release-script.test.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'fs' +import { join } from 'path' +import { describe, expect, it } from 'vitest' + +describe('release script terminal custody', () => { + it('keeps npm verification/version work out of interactive shell job control', () => { + const source = readFileSync(join(__dirname, '../../scripts/release.mjs'), 'utf8') + + expect(source).toContain("stdio: ['ignore', 'inherit', 'inherit']") + expect(source).toContain("env: { ...process.env, CI: '1' }") + expect(source).toContain("const git = args => execFileSync('git', args, { stdio: 'inherit' })") + }) +})