Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion scripts/release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions src/main/release-script.test.ts
Original file line number Diff line number Diff line change
@@ -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' })")
})
})
Loading