Files
msd-core/docs/contributing/bootstrap.md
Tom Boucher 48b1e35187 fix(#431): enforce H1 shell policy (linux=bash, macOS=zsh, windows=pwsh) across PR + release gates (#434)
* test(#431): policy-shell-pinning linter — RED baseline (37 violations on origin/next)

Adds scripts/workflow-policy.cjs: H1 shell-policy linter with POLICY map,
VIOLATION enum, matrix expansion, effective-shell resolution order, and
runPolicyLint({ workflowsDir }) entry point.

Adds tests/policy-shell-pinning.test.cjs: 8 tests (baseline + 6 synthetic
counter-tests). Synthetic tests 2–7 pass; baseline test is intentionally RED
(37 violations: 28 in test.yml, 9 in install-smoke.yml — all macos/windows
lanes using shell: bash instead of native zsh/pwsh).

Adds js-yaml@4.1.1 as devDependency for YAML parsing.

* fix(#431): switch ubuntu/windows lanes to native shells; extract bash-isms to Node

Remove all explicit shell: bash pins from ubuntu-only jobs (changes, lint-tests,
coverage, required-tests, smoke-unpacked) — ubuntu runner default is bash, which
is both H1-compliant and the runner default, making the pin redundant.

For the test and test-full mixed-OS jobs (ubuntu+windows, windows+macos):
- Move bash-ism steps to shell-agnostic Node scripts:
    scripts/ci-guard-runner.cjs       — RUNNER_ENVIRONMENT check
    scripts/ci-rebase-check.cjs       — git fetch+merge PR base branch
    scripts/check-npm-integrity.cjs   — Node port of check-npm-integrity.sh
    scripts/ci-prepare-test-scope.cjs — write .ci-selected-tests.txt
    scripts/ci-smoke-skip.cjs         — set skip= output for full-only matrix entries
- Remove shell: bash from simple npm/node command steps (runner default applies)

This brings Windows violations from 19 to 0. Remaining 17 violations are all
MACOS_MISSING_EXPLICIT_ZSH in mixed-OS matrix jobs (test-full: windows+macos,
install-smoke smoke: ubuntu+macos) — these require job splitting to fix; see
BLOCKER in PR description.

* fix(#431): update workflow-shell-pinning test for H1 policy

The old test required all Windows-targeting npm steps to pin shell: bash
(to prevent pwsh stderr-swallow). Under H1, Windows runners must use
pwsh (native, no pin needed) — shell: bash on Windows is now the
violation, not the fix.

Update findViolations() to flag npm steps with effectiveShell === 'bash'
(rather than effectiveShell === null). Update synthetic tests to verify
the H1-inverted semantics: defaults.run.shell: bash on Windows is now 2
violations, not 0. Update test name and assertion messages to describe
the H1 constraint rather than the old missing-pin constraint.

* fix(#431): extend policy linter to resolve matrix.shell expressions

- expandRunsOn now captures all matrix.include row keys as realization
  context (os, node-version, shell, full_only, etc.) instead of only os
- effectiveShell now accepts a realizationContext and resolves
  ${{ matrix.<key> }} expressions against it before checking policy
- Unresolvable matrix key in shell expression emits UNRESOLVABLE_MATRIX
- Add 3 new tests: positive (zsh+pwsh per row → 0 violations),
  counter (bash in macOS row → WRONG_SHELL_FOR_OS), counter (missing
  shell key → UNRESOLVABLE_MATRIX)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#431): apply matrix.shell pattern to test-full and smoke jobs (clears BLOCKER)

test-full job (test.yml):
- Add shell: pwsh/zsh per matrix.include row (windows-latest→pwsh,
  macos-latest→zsh)
- Add job-level defaults.run.shell: ${{ matrix.shell }}
- No step-level shell pins existed to remove

smoke job (install-smoke.yml):
- Add shell: bash/zsh per matrix.include row (ubuntu→bash, macos→zsh)
- Add job-level defaults.run.shell: ${{ matrix.shell }}
- No step-level shell pins existed to remove

Policy linter now reports 0 violations across all workflow files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(#431): migrate .sh check scripts to .cjs; remove .sh originals

- Add scripts/check-env.cjs: Node.js port of check-env.sh with
  identical exit codes (0/1/2), human-readable and --json output,
  --help flag, and all 5 checks (node-version, npm-version,
  lockfile-present, lockfile-sync, version-manager-pin)
- Migrate all callers:
  - package.json check:env → node scripts/check-env.cjs
  - package.json check:integrity → node scripts/check-npm-integrity.cjs
  - scripts/ci-test-scope.cjs path strings → .cjs equivalents
  - .github/workflows/release.yml rc+finalize jobs → node .cjs (drop chmod+x)
  - .github/workflows/security-scan.yml → node .cjs (drop chmod+x)
  - tests/check-env.test.cjs → spawn node process.execPath [.cjs]
  - tests/npm-integrity-gate.test.cjs → spawn node process.execPath [.cjs]
- Delete scripts/check-env.sh and scripts/check-npm-integrity.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(#431): update doc references from .sh to .cjs

Update SECURITY.md and docs/contributing/bootstrap.md to reference the
canonical Node invocation instead of the removed bash scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#431): use per-step shell:matrix.shell instead of defaults.run.shell (GHA compat)

GHA does not reliably resolve matrix expressions inside defaults.run.shell.
Per-step shell: always resolves correctly. Removed the defaults.run.shell block
from the test-full job (test.yml) and the smoke job (install-smoke.yml), and
added shell: \${{ matrix.shell }} directly on every run: step in both jobs.

Codex finding: defaults.run.shell with matrix expressions is not a
GHA-supported pattern; per-step shell: is the safe form.

* fix(#431): policy linter validates every matrix.include row independently

Removed runner-label-only dedup from expandRunsOn() in workflow-policy.cjs.
The prior guard (if !realizations.find(r => r.runner === runner)) collapsed
two macos-latest rows with different node-version/shell contexts into one,
hiding the second row's policy violation.

Each matrix.include row is a distinct CI realization with its own context;
validating it twice is harmless but skipping it causes false negatives.

Added counter-test (Test 8) in tests/policy-shell-pinning.test.cjs:
two macos-latest rows (shell:zsh compliant + shell:bash violation) must
produce exactly one WRONG_SHELL_FOR_OS violation on the second row.

* fix(#431): remove dedup-by-runner in Cartesian matrix.<key> expansion (Codex round 3)

The base-list path in expandRunsOn (matrix.<key> arrays, e.g. matrix.os)
previously guarded each push with `if (!realizations.find(r => r.runner === runner))`,
collapsing duplicate runner values into a single realization and hiding policy
violations on later rows of a Cartesian matrix.

Remove the guard unconditionally; each entry in the base-list array now produces
its own realization, matching the same fix already applied to the matrix.include path.

Add counter-test "Cartesian matrix os × shell — dedup must not collapse rows by
runner alone": matrix.os: [macos-latest, macos-latest] + shell: ${{ matrix.shell }}
now yields 2 realizations (not 1). Documents that Cartesian cross-product expansion
(carrying all keys into realization context) is a separate follow-up; current violations
are UNRESOLVABLE_MATRIX pending that work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#431): remove 60s timeout regression on npm ci --dry-run (parity with check-env.sh)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#431): ci-rebase-check.cjs — return truthy sentinel on success (Codex round 4)

run() used execFileSync with stdio:'inherit', which returns null on success.
Caller checked `result !== null`, always false → every successful fetch fell
through to "failed after 3 attempts" exit-1 path.

Fix: run() now returns true on success, false on failure.
Update caller from `result !== null` to `if (result)`.

Adds tests/ci-rebase-check.test.cjs (5 tests) covering the sentinel contract
and a local-bare-remote integration smoke that verifies the full fetch+merge
path exits 0 when fetch succeeds.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: CI Rebase Check <ci@gsd-redux>
2026-05-28 09:23:59 -04:00

6.4 KiB

Bootstrap your environment

This guide gets a new contributor from a fresh checkout to a passing baseline in one session.

Sources:


Prerequisites

Node version manager (pick one)

Tool Install Docs
nvm (recommended) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/install.sh | bash https://github.com/nvm-sh/nvm
fnm (fast, Rust) curl -fsSL https://fnm.vercel.app/install | bash https://github.com/Schniz/fnm
asdf brew install asdf then asdf plugin add nodejs https://asdf-vm.com
mise curl https://mise.run | sh https://mise.jdx.dev

A version manager ensures you can switch Node versions per project without polluting your global install. This project ships a .nvmrc file at the root — any of the tools above will read it.

Other required tools

  • gh (GitHub CLI) — https://cli.github.com — used by contribution workflows and CI
  • git — any recent version

One-time setup

# 1. Clone
git clone https://github.com/open-gsd/get-shit-done-redux.git
cd get-shit-done-redux

# 2. Activate the pinned Node version
nvm use          # nvm
# fnm use        # fnm
# asdf install   # asdf / mise

# 3. Verify the environment (see Validation below)
npm run check:env

# 4. Install dependencies (reproducible, lockfile-driven)
npm ci

npm ci is required over npm install. It installs exactly what package-lock.json specifies and fails fast if the lockfile is out of sync — this is intentional. See https://docs.npmjs.com/cli/v10/commands/npm-ci


Daily commands

Command Purpose
npm run check:env Validate your environment before running tests
npm test Run the full test suite (unit + integration + security)
npm run test:unit Unit tests only (fastest)
npm run test:integration Integration tests
npm run build:sdk Rebuild the SDK dist (required before first test run)

npm run check:integrity — available once #114 merges.


Validation

Run the environment validator before any test or audit run:

npm run check:env

This runs scripts/check-env.cjs and reports pass/fail for each check:

Check What it verifies
node-version Active Node satisfies engines.node (>=22.0.0)
npm-version Active npm satisfies engines.npm (>=10.0.0)
lockfile-present package-lock.json exists at root
lockfile-sync npm ci --dry-run exits 0 (lockfile matches installed state)
version-manager-pin Active Node major matches .nvmrc / .node-version / .tool-versions

Exit codes:

  • 0 — all checks passed, safe to proceed
  • 1 — one or more checks failed — see the report
  • 2 — tool error (e.g., node not found, corrupt package.json)

For structured output (useful in scripts):

npm run check:env -- --json

Troubleshooting

node-version FAIL — Node X does NOT satisfy >=22.0.0

Cause: The system Node is too old, or the version manager hasn't activated the correct version.

Fix:

nvm use          # activates version from .nvmrc
node --version   # confirm

If nvm reports the version is not installed:

nvm install      # installs the version in .nvmrc
nvm use

npm-version FAIL — npm X does NOT satisfy >=10.0.0

Cause: npm bundled with an old Node version.

Fix:

npm install -g npm@latest
npm --version

lockfile-present FAIL — package-lock.json missing

Cause: The lockfile was deleted or was never generated.

Fix:

npm install      # generates package-lock.json

Do NOT commit a regenerated lockfile without verifying no unexpected packages changed. Run git diff package-lock.json to inspect the diff.


lockfile-sync FAIL — package-lock.json is out of sync

Cause: package.json was edited (dependency added/changed) without updating the lockfile, or the lockfile was hand-edited.

Fix:

npm ci           # restores node_modules to match lockfile exactly
# or, if the sync failure is intentional (you updated package.json):
npm install      # updates lockfile to match package.json

version-manager-pin FAIL — Active Node major does not match .nvmrc

Cause: The shell is using a globally-installed Node rather than the version manager's activation. Common on fresh shell sessions.

Fix:

nvm use          # re-activate from .nvmrc
# or add to your shell profile:
# echo 'nvm use --silent' >> ~/.zshrc

Tests fail with Error: Cannot find module ...

Cause: node_modules is missing or stale (common after a branch switch that changed package.json).

Fix:

npm ci           # clean install from lockfile
npm run build:sdk

Locale / encoding errors on non-UTF-8 systems

Cause: Some test fixtures contain non-ASCII characters. Node requires a UTF-8 locale.

Fix:

export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

Add these to your shell profile to make them permanent.


Alternative: Docker via gsd-test-runner

For canonical Linux verification from a macOS dev box, use gsd-test-runner.

This is the same path CI uses for cross-platform coverage. It is the authoritative way to confirm your change passes on Linux before opening a PR:

# One-time: install gsd-test-runner (see repo README)
# Then, from the project root:
gsd-test-summary

gsd-test-summary runs the full suite in a Docker container and emits a concise Mac: N failed / Docker: N failed summary.

  • Default rule (code changes): both lines must show 0 failed before a PR is opened.
  • Exception (ADR/doc-only PRs): if the diff is documentation-only (for example docs/adr/*.md, docs/**/*.md, README*.md) and contains no executable-code or test changes, gsd-test-summary is optional.

When using the doc-only exception, note it explicitly in the PR body (for example: "Doc-only PR; gsd-test-summary not required by docs-only exception in docs/contributing/bootstrap.md").