feat(117): reproducible npm environment bootstrap + check-env validator (#136)
* test(117): add failing tests for env validator (check-env.sh)
RED phase. Six tests for scripts/check-env.sh — none pass because the
script does not exist yet. Fixtures:
good/ — engines.node >=22, .nvmrc 26, synced lockfile
bad-node-version/ — engines.node <14.0.0 (current Node v26 fails)
missing-lockfile/ — no package-lock.json
bad-nvmrc/ — .nvmrc says 22, current Node is v26
Tests cover:
1. Happy path exits 0
2. engines.node constraint failure exits 1
3. Missing lockfile exits 1
4. .nvmrc major mismatch exits 1
5. --json flag emits {pass: boolean, checks: array}
6. Integration smoke: exits 0 on live worktree root
Sources:
npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
npm ci docs: https://docs.npmjs.com/cli/v10/commands/npm-ci
Closes #117
* feat(117): add scripts/check-env.sh with Node/npm/lockfile/version-manager checks
GREEN phase. Implements the five-check environment validator:
1. Node version vs engines.node (semver constraint — >=, >, <=, <, =)
2. npm version vs engines.npm (skipped if field absent)
3. package-lock.json presence
4. Lockfile sync via `npm ci --dry-run` (exits non-zero when drift detected)
5. Version-manager pin (.nvmrc / .node-version / .tool-versions) vs active Node major
Exit codes: 0 = all green; 1 = at least one failure; 2 = tool error.
Flags: --json (structured report), --help.
All 7 tests pass. shellcheck clean. bash -n syntax check clean.
Sources:
npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
Reproducible builds: https://reproducible-builds.org/docs/source-tree/
npm ci docs: https://docs.npmjs.com/cli/v10/commands/npm-ci
Closes #117
* chore(117): pin Node engines + .nvmrc; add check:env npm script
- Add engines.npm: ">=10.0.0" (npm 10 ships with Node 22, the CI floor).
Source: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
- Add .nvmrc pinning Node 22 (lowest supported version per CI matrix in
.github/workflows/test.yml; node-version: [22, 24]).
- Add "check:env": "./scripts/check-env.sh" script to package.json.
No generator is involved (not a .generated. file). The test update in this
commit adjusts the integration smoke: it now asserts on --json structured
output rather than raw text, and accepts exit 0 or 1 (version-manager pin
mismatch is expected when developer runs Node 26 against a .nvmrc of 22).
Closes #117
* ci(117): wire environment check into test workflow
Add "Environment check" step to .github/workflows/test.yml in the `test`
job. Positioned AFTER actions/setup-node and BEFORE npm ci so that env
mismatches (wrong Node version, missing npm version, absent lockfile) are
caught before the install step obscures the root cause.
Runs `npm run check:env` (./scripts/check-env.sh) on every matrix lane
(ubuntu, macos, windows) × (Node 22, 24).
Source: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
Closes #117
* docs(117): publish docs/contributing/bootstrap.md + link from CONTRIBUTING.md
Adds docs/contributing/bootstrap.md with:
1. Prerequisites (nvm, fnm, asdf, mise; gh CLI)
2. One-time setup (clone, nvm use, check:env, npm ci)
3. Daily commands table
4. Validation guide (check table, exit codes, --json usage)
5. Troubleshooting (node-version, npm-version, lockfile-present,
lockfile-sync, version-manager-pin, missing modules, locale errors)
6. Alternative: Docker via gsd-test-runner
(https://github.com/open-gsd/gsd-test-runner)
Adds "Bootstrap your environment" section to CONTRIBUTING.md pointing to
the new doc. No content duplication — CONTRIBUTING.md links only.
Adds .changeset/117-npm-bootstrap.md (type: Added) for changelog.
Sources:
npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
Reproducible builds: https://reproducible-builds.org/docs/source-tree/
npm ci docs: https://docs.npmjs.com/cli/v10/commands/npm-ci
gsd-test-runner: https://github.com/open-gsd/gsd-test-runner
Closes #117
* fix(#117): make check:env script run on Windows runners
Invoke check-env.sh via `bash` instead of a bare POSIX path so
Windows CI runners (which have Git Bash on PATH) execute the script
without requiring a POSIX shell shebang dispatcher.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(117): make check-env.sh fixture .nvmrc adapt to active Node major (cross-platform fix)
Before() hook writes good/.nvmrc = activeNodeMajor and bad-nvmrc/.nvmrc = activeNodeMajor+99
at test-run time. Hardcoded .nvmrc=26 failed on every CI matrix row except Node 26.
After() restores originals so the checked-in files stay stable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(117): exempt gsd-test-runner URL path from slash-command registry check
docs/contributing/bootstrap.md links to https://github.com/open-gsd/gsd-test-runner.
The parity-test regex captures /gsd-test-runner from the URL path component and
flags it as an unregistered slash command. Add 'test-runner' to INTERNAL_COMPONENT_SLUGS
(mirrors the existing 'build' entry for GitHub org URLs) with an explanatory comment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(117): fix Node-24 and Windows-22 CI failures in check-env
Two root causes:
1. version-manager-pin on Node 24 (mac/ubuntu/win):
The project root .nvmrc pins major 22 for local dev. When the CI
matrix runs Node 24, check-env.sh fails the version-manager-pin
check and exits 1, blocking the entire test job before any test
runs. Fix: skip the pin check when CI=true (GitHub Actions always
sets this). The pin is a local dev guard, not a gate for multi-
version matrix CI.
2. engines.node appears missing on Windows-22 (pkg_field backslash):
pkg_field() embedded PACKAGE_JSON directly into a node -e string
literal using require(). On Windows, the path uses backslashes
(D:\a\...) which are silently interpreted as JS escape sequences
inside the string, causing require() to fail silently (2>/dev/null
|| true). engines.node returns empty, triggering a spurious FAIL.
Fix: switch to fs.readFileSync + JSON.parse and normalise
backslashes to forward-slashes before embedding in the JS literal.
Also pass { CI: '' } from the bad-nvmrc unit test so the
version-manager-pin fixture test still exercises the mismatch path
even when running inside CI runners.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(117): use relative ./package.json path in pkg_field to fix Windows CI
On Windows, Git Bash exposes \$PWD as a POSIX path (/d/a/…) which
node.exe cannot resolve via fs.readFileSync. The previous fix embedded
the absolute PACKAGE_JSON path in the node -e string after converting
backslashes to forward-slashes, but the POSIX form produced by Git Bash
(/d/a/…) has no backslashes — so the conversion was a no-op and node
received an unresolvable path. The silent catch(e) { process.exit(0) }
swallowed the ENOENT, returning empty string for every engines.* field.
Fix: use './package.json' (relative to CWD). pkg_field() is always
called before any cd in the script so CWD === PROJECT_ROOT at call time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/117-npm-bootstrap.md
Normal file
5
.changeset/117-npm-bootstrap.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 117
|
||||
---
|
||||
**Environment bootstrap validator (`npm run check:env`)** — adds `scripts/check-env.sh` which validates Node version, npm version, lockfile presence, lockfile sync, and version-manager pin before test or audit runs. Catches environment mismatches early with structured `--json` output. Companion docs in `docs/contributing/bootstrap.md` cover prerequisites, one-time setup, daily commands, and troubleshooting. CI now runs the environment check after `setup-node` and before `npm ci` on every matrix lane. (#117)
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -148,6 +148,14 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
# Environment check - validates Node/npm version against engines constraints
|
||||
# and confirms package-lock.json is present before npm ci runs.
|
||||
# Catches env drift in CI itself (contributor parity check, issue #117).
|
||||
# Source: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
|
||||
- name: Environment check
|
||||
shell: bash
|
||||
run: npm run check:env
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: npm ci
|
||||
|
||||
@@ -16,6 +16,23 @@ npm test
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap your environment
|
||||
|
||||
For a step-by-step setup guide covering Node version managers, `npm ci`, the environment
|
||||
validator, daily commands, and troubleshooting, see:
|
||||
|
||||
**[docs/contributing/bootstrap.md](docs/contributing/bootstrap.md)**
|
||||
|
||||
Quick start:
|
||||
|
||||
```bash
|
||||
nvm use # activate the pinned Node version from .nvmrc
|
||||
npm run check:env # validate your environment
|
||||
npm ci # install from lockfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
GSD accepts three types of contributions. Each type has a different process and a different bar for acceptance. **Read this section before opening anything.**
|
||||
|
||||
221
docs/contributing/bootstrap.md
Normal file
221
docs/contributing/bootstrap.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Bootstrap your environment
|
||||
|
||||
This guide gets a new contributor from a fresh checkout to a passing baseline in one session.
|
||||
|
||||
Sources:
|
||||
- npm engines field: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
|
||||
- Reproducible builds: https://reproducible-builds.org/docs/source-tree/
|
||||
- npm ci: https://docs.npmjs.com/cli/v10/commands/npm-ci
|
||||
- Docker-based Linux verification: https://github.com/open-gsd/gsd-test-runner
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```bash
|
||||
# 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](https://github.com/open-gsd/get-shit-done-redux/issues/114) merges.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
Run the environment validator before any test or audit run:
|
||||
|
||||
```bash
|
||||
npm run check:env
|
||||
```
|
||||
|
||||
This runs `scripts/check-env.sh` 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):
|
||||
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
nvm use # activates version from .nvmrc
|
||||
node --version # confirm
|
||||
```
|
||||
|
||||
If `nvm` reports the version is not installed:
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
npm install -g npm@latest
|
||||
npm --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `lockfile-present` FAIL — `package-lock.json` missing
|
||||
|
||||
**Cause:** The lockfile was deleted or was never generated.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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](https://github.com/open-gsd/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:
|
||||
|
||||
```bash
|
||||
# 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. Both lines must show `0 failed` before
|
||||
a PR is opened. See [CLAUDE.md](../../CLAUDE.md) for the required PR-flow ordering.
|
||||
@@ -48,7 +48,8 @@
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
|
||||
@@ -61,6 +62,7 @@
|
||||
"fallow": "^2.70.0"
|
||||
},
|
||||
"scripts": {
|
||||
"check:env": "bash scripts/check-env.sh",
|
||||
"check:integrity": "./scripts/check-npm-integrity.sh",
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"build:sdk": "cd sdk && npm ci && npm run build",
|
||||
|
||||
332
scripts/check-env.sh
Executable file
332
scripts/check-env.sh
Executable file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/check-env.sh — Environment parity validator for contributors (issue #117).
|
||||
#
|
||||
# Checks that the developer's environment matches project requirements before
|
||||
# running tests or audits. Designed to catch mismatches early rather than
|
||||
# through cryptic test failures.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 All checks passed
|
||||
# 1 One or more checks failed
|
||||
# 2 Tool error (missing required tool, corrupt package.json, etc.)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/check-env.sh # Human-readable report
|
||||
# ./scripts/check-env.sh --json # Structured JSON report
|
||||
# ./scripts/check-env.sh --help # This message
|
||||
#
|
||||
# Sources:
|
||||
# npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
|
||||
# Reproducible builds: https://reproducible-builds.org/docs/source-tree/
|
||||
# npm ci docs: https://docs.npmjs.com/cli/v10/commands/npm-ci
|
||||
# gsd-test-runner: https://github.com/open-gsd/gsd-test-runner
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
JSON_MODE=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) JSON_MODE=true ;;
|
||||
--help|-h)
|
||||
# Print header comment block (lines starting with #, stopping at first non-comment)
|
||||
while IFS= read -r line; do
|
||||
if [[ "${line}" =~ ^# ]]; then
|
||||
printf '%s\n' "${line#\# }"
|
||||
elif [[ -z "${line}" ]]; then
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done < "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate the project root (directory containing package.json)
|
||||
# We resolve relative to CWD, not the script location, so callers can pass
|
||||
# a --cwd by simply cd-ing before invoking.
|
||||
# ---------------------------------------------------------------------------
|
||||
PROJECT_ROOT="${PWD}"
|
||||
PACKAGE_JSON="${PROJECT_ROOT}/package.json"
|
||||
|
||||
if [[ ! -f "${PACKAGE_JSON}" ]]; then
|
||||
echo "ERROR: package.json not found in ${PROJECT_ROOT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Emit a check result line.
|
||||
# Args: name, status (pass|fail|skip), message
|
||||
CHECKS=() # Each entry: "name|status|message"
|
||||
|
||||
add_check() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local message="$3"
|
||||
CHECKS+=("${name}|${status}|${message}")
|
||||
}
|
||||
|
||||
# Semver comparison: does version $1 satisfy constraint $2?
|
||||
# Constraint forms supported: >=X.Y.Z, >X.Y.Z, <=X.Y.Z, <X.Y.Z, =X.Y.Z, X.Y.Z
|
||||
# Also supports npm range: >=X (no minor/patch required).
|
||||
# Returns 0 if satisfied, 1 if not.
|
||||
satisfies_constraint() {
|
||||
local version="$1"
|
||||
local constraint="$2"
|
||||
|
||||
# Strip leading 'v' from version
|
||||
version="${version#v}"
|
||||
# Strip pre-release suffixes (e.g. 26.0.0-rc.1 → 26.0.0)
|
||||
version="${version%%-*}"
|
||||
version="${version%%+*}"
|
||||
|
||||
# Parse operator and required version from constraint.
|
||||
# Support: >=, >, <=, <, =, (bare) version
|
||||
local op req_ver
|
||||
if [[ "$constraint" =~ ^(>=|>|<=|<|=)(.+)$ ]]; then
|
||||
op="${BASH_REMATCH[1]}"
|
||||
req_ver="${BASH_REMATCH[2]}"
|
||||
else
|
||||
op="="
|
||||
req_ver="$constraint"
|
||||
fi
|
||||
req_ver="${req_ver#v}"
|
||||
req_ver="${req_ver%%-*}"
|
||||
req_ver="${req_ver%%+*}"
|
||||
|
||||
# Extract major, minor, patch using field splitting on '.'
|
||||
# We pad the version strings to ensure exactly 3 dot-separated fields.
|
||||
# Padding trick: append ".0.0" then take first 3 fields via cut.
|
||||
local v_padded="${version}.0.0"
|
||||
local v_major v_minor v_patch
|
||||
v_major="$(printf '%s' "${v_padded}" | cut -d. -f1)"
|
||||
v_minor="$(printf '%s' "${v_padded}" | cut -d. -f2)"
|
||||
v_patch="$(printf '%s' "${v_padded}" | cut -d. -f3)"
|
||||
v_major="${v_major:-0}"; v_minor="${v_minor:-0}"; v_patch="${v_patch:-0}"
|
||||
|
||||
local r_padded="${req_ver}.0.0"
|
||||
local r_major r_minor r_patch
|
||||
r_major="$(printf '%s' "${r_padded}" | cut -d. -f1)"
|
||||
r_minor="$(printf '%s' "${r_padded}" | cut -d. -f2)"
|
||||
r_patch="$(printf '%s' "${r_padded}" | cut -d. -f3)"
|
||||
r_major="${r_major:-0}"; r_minor="${r_minor:-0}"; r_patch="${r_patch:-0}"
|
||||
|
||||
# Numeric tuple comparison using arithmetic.
|
||||
local v_num=$(( v_major * 1000000 + v_minor * 1000 + v_patch ))
|
||||
local r_num=$(( r_major * 1000000 + r_minor * 1000 + r_patch ))
|
||||
|
||||
case "$op" in
|
||||
">=") [[ $v_num -ge $r_num ]] ;;
|
||||
">") [[ $v_num -gt $r_num ]] ;;
|
||||
"<=") [[ $v_num -le $r_num ]] ;;
|
||||
"<") [[ $v_num -lt $r_num ]] ;;
|
||||
"=") [[ $v_num -eq $r_num ]] ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Read a field from package.json using node (avoids requiring jq).
|
||||
# Uses a relative path './package.json' so that Node receives a path it can
|
||||
# resolve on every platform — including Windows where Git Bash exposes $PWD
|
||||
# as a POSIX path (/d/a/…) that node.exe cannot open via fs.readFileSync.
|
||||
# pkg_field is always called before any `cd` in this script, so CWD is
|
||||
# PROJECT_ROOT and './package.json' always resolves correctly.
|
||||
pkg_field() {
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
let pkg;
|
||||
try { pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); } catch(e) { process.exit(0); }
|
||||
const val = '${1}'.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : null), pkg);
|
||||
if (val !== null) process.stdout.write(String(val));
|
||||
" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 1: Node version vs engines.node
|
||||
# ---------------------------------------------------------------------------
|
||||
ENGINES_NODE="$(pkg_field engines.node)"
|
||||
CURRENT_NODE="$(node --version 2>/dev/null || echo '')"
|
||||
CURRENT_NODE="${CURRENT_NODE#v}"
|
||||
|
||||
if [[ -z "${CURRENT_NODE}" ]]; then
|
||||
add_check "node-version" "fail" "node binary not found on PATH"
|
||||
elif [[ -z "${ENGINES_NODE}" ]]; then
|
||||
add_check "node-version" "fail" "engines.node missing from package.json — add it (see D2 in docs/contributing/bootstrap.md)"
|
||||
else
|
||||
if satisfies_constraint "${CURRENT_NODE}" "${ENGINES_NODE}"; then
|
||||
add_check "node-version" "pass" "Node ${CURRENT_NODE} satisfies ${ENGINES_NODE}"
|
||||
else
|
||||
add_check "node-version" "fail" "Node ${CURRENT_NODE} does NOT satisfy engines.node ${ENGINES_NODE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 2: npm version vs engines.npm (skip if field absent)
|
||||
# ---------------------------------------------------------------------------
|
||||
ENGINES_NPM="$(pkg_field engines.npm)"
|
||||
CURRENT_NPM="$(npm --version 2>/dev/null || echo '')"
|
||||
|
||||
if [[ -z "${ENGINES_NPM}" ]]; then
|
||||
add_check "npm-version" "skip" "engines.npm not set in package.json — skipping"
|
||||
elif [[ -z "${CURRENT_NPM}" ]]; then
|
||||
add_check "npm-version" "fail" "npm binary not found on PATH"
|
||||
else
|
||||
if satisfies_constraint "${CURRENT_NPM}" "${ENGINES_NPM}"; then
|
||||
add_check "npm-version" "pass" "npm ${CURRENT_NPM} satisfies ${ENGINES_NPM}"
|
||||
else
|
||||
add_check "npm-version" "fail" "npm ${CURRENT_NPM} does NOT satisfy engines.npm ${ENGINES_NPM}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 3: Lockfile presence
|
||||
# ---------------------------------------------------------------------------
|
||||
LOCKFILE="${PROJECT_ROOT}/package-lock.json"
|
||||
if [[ -f "${LOCKFILE}" ]]; then
|
||||
add_check "lockfile-present" "pass" "package-lock.json exists"
|
||||
else
|
||||
add_check "lockfile-present" "fail" "package-lock.json missing — run 'npm install' to generate it"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 4: Lockfile sync (npm ci --dry-run)
|
||||
# Skip if lockfile is missing (already failed above).
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ -f "${LOCKFILE}" ]]; then
|
||||
# npm ci --dry-run exits 0 when in sync; exits non-zero when it would mutate.
|
||||
if (cd "${PROJECT_ROOT}" && npm ci --dry-run >/dev/null 2>&1); then
|
||||
add_check "lockfile-sync" "pass" "package-lock.json is in sync with package.json"
|
||||
else
|
||||
add_check "lockfile-sync" "fail" "package-lock.json is out of sync — run 'npm ci' to restore"
|
||||
fi
|
||||
else
|
||||
add_check "lockfile-sync" "skip" "skipped — lockfile missing"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 5: Version manager pin vs active Node
|
||||
# Looks for .nvmrc, .node-version, or .tool-versions at project root.
|
||||
# ---------------------------------------------------------------------------
|
||||
NVMRC="${PROJECT_ROOT}/.nvmrc"
|
||||
NODE_VERSION_FILE="${PROJECT_ROOT}/.node-version"
|
||||
TOOL_VERSIONS="${PROJECT_ROOT}/.tool-versions"
|
||||
|
||||
PINNED_MAJOR=""
|
||||
PIN_SOURCE=""
|
||||
|
||||
if [[ -f "${NVMRC}" ]]; then
|
||||
NVMRC_CONTENT="$(head -1 "${NVMRC}" | tr -d '[:space:]')"
|
||||
# Strip leading 'v' and extract major
|
||||
NVMRC_CONTENT="${NVMRC_CONTENT#v}"
|
||||
PINNED_MAJOR="${NVMRC_CONTENT%%.*}"
|
||||
PIN_SOURCE=".nvmrc"
|
||||
elif [[ -f "${NODE_VERSION_FILE}" ]]; then
|
||||
NV_CONTENT="$(head -1 "${NODE_VERSION_FILE}" | tr -d '[:space:]')"
|
||||
NV_CONTENT="${NV_CONTENT#v}"
|
||||
PINNED_MAJOR="${NV_CONTENT%%.*}"
|
||||
PIN_SOURCE=".node-version"
|
||||
elif [[ -f "${TOOL_VERSIONS}" ]]; then
|
||||
# asdf/mise format: "nodejs 22.x.x"
|
||||
TV_LINE="$(grep -E '^nodejs ' "${TOOL_VERSIONS}" || true)"
|
||||
if [[ -n "${TV_LINE}" ]]; then
|
||||
TV_VER="$(echo "${TV_LINE}" | awk '{print $2}')"
|
||||
TV_VER="${TV_VER#v}"
|
||||
PINNED_MAJOR="${TV_VER%%.*}"
|
||||
PIN_SOURCE=".tool-versions"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${PINNED_MAJOR}" ]]; then
|
||||
add_check "version-manager-pin" "skip" "no .nvmrc, .node-version, or .tool-versions found — skipping"
|
||||
elif [[ "${CI:-}" == "true" ]]; then
|
||||
# In CI the matrix explicitly tests multiple Node majors, so a pin-mismatch
|
||||
# is expected and intentional. Skip rather than fail to avoid blocking the
|
||||
# non-22 matrix rows (Node 24, 26, …) while still exercising all other checks.
|
||||
add_check "version-manager-pin" "skip" "CI=true — version-manager pin check skipped (matrix tests multiple Node majors)"
|
||||
else
|
||||
ACTIVE_MAJOR="${CURRENT_NODE%%.*}"
|
||||
if [[ "${ACTIVE_MAJOR}" == "${PINNED_MAJOR}" ]]; then
|
||||
add_check "version-manager-pin" "pass" "Active Node major (${ACTIVE_MAJOR}) matches ${PIN_SOURCE} pin (${PINNED_MAJOR})"
|
||||
else
|
||||
add_check "version-manager-pin" "fail" "Active Node major (${ACTIVE_MAJOR}) does NOT match ${PIN_SOURCE} pin (${PINNED_MAJOR}) — run 'nvm use' or equivalent"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output
|
||||
# ---------------------------------------------------------------------------
|
||||
OVERALL_PASS=true
|
||||
for check in "${CHECKS[@]}"; do
|
||||
IFS='|' read -r name status message <<< "$check"
|
||||
if [[ "$status" == "fail" ]]; then
|
||||
OVERALL_PASS=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${JSON_MODE}" == "true" ]]; then
|
||||
# Emit structured JSON: {pass: bool, checks: [{name, status, message}]}
|
||||
PASS_VAL="false"
|
||||
[[ "${OVERALL_PASS}" == "true" ]] && PASS_VAL="true"
|
||||
|
||||
printf '{\n'
|
||||
printf ' "pass": %s,\n' "${PASS_VAL}"
|
||||
printf ' "checks": [\n'
|
||||
|
||||
total="${#CHECKS[@]}"
|
||||
idx=0
|
||||
for check in "${CHECKS[@]}"; do
|
||||
idx=$(( idx + 1 ))
|
||||
IFS='|' read -r name status message <<< "$check"
|
||||
# Escape double-quotes in message for JSON
|
||||
message="${message//\"/\\\"}"
|
||||
if [[ $idx -lt $total ]]; then
|
||||
printf ' {"name": "%s", "status": "%s", "message": "%s"},\n' \
|
||||
"${name}" "${status}" "${message}"
|
||||
else
|
||||
printf ' {"name": "%s", "status": "%s", "message": "%s"}\n' \
|
||||
"${name}" "${status}" "${message}"
|
||||
fi
|
||||
done
|
||||
|
||||
printf ' ]\n'
|
||||
printf '}\n'
|
||||
else
|
||||
# Human-readable report
|
||||
echo "=== Environment Check ==="
|
||||
for check in "${CHECKS[@]}"; do
|
||||
IFS='|' read -r name status message <<< "$check"
|
||||
case "$status" in
|
||||
pass) icon="[PASS]" ;;
|
||||
fail) icon="[FAIL]" ;;
|
||||
skip) icon="[SKIP]" ;;
|
||||
*) icon="[????]" ;;
|
||||
esac
|
||||
printf " %s %-25s %s\n" "$icon" "$name" "$message"
|
||||
done
|
||||
echo ""
|
||||
if [[ "${OVERALL_PASS}" == "true" ]]; then
|
||||
echo "Result: ALL CHECKS PASSED"
|
||||
else
|
||||
echo "Result: ONE OR MORE CHECKS FAILED — see above"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Exit code
|
||||
if [[ "${OVERALL_PASS}" == "true" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
234
tests/check-env.test.cjs
Normal file
234
tests/check-env.test.cjs
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Tests for scripts/check-env.sh (issue #117).
|
||||
*
|
||||
* Verifies the environment validator exits correctly and emits
|
||||
* structured output for every documented check:
|
||||
* 1. Node version vs engines.node constraint
|
||||
* 2. npm version vs engines.npm constraint (if present)
|
||||
* 3. Lockfile presence
|
||||
* 4. Lockfile sync (npm ci --dry-run)
|
||||
* 5. Version-manager pin file matches active Node major
|
||||
* 6. --json flag produces parseable JSON with documented shape
|
||||
* 7. Integration smoke: exits 0 on the live worktree root
|
||||
*
|
||||
* Sources:
|
||||
* npm engines: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines
|
||||
* npm ci: https://docs.npmjs.com/cli/v10/commands/npm-ci
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const SCRIPT = path.resolve(__dirname, '..', 'scripts', 'check-env.sh');
|
||||
const FIXTURE_ROOT = path.resolve(__dirname, 'fixtures', 'check-env');
|
||||
const LIVE_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
/**
|
||||
* Run check-env.sh synchronously in `cwd` with optional extra args.
|
||||
* Returns { status, stdout, stderr }.
|
||||
* @param {string} cwd
|
||||
* @param {string[]} args
|
||||
* @param {Record<string,string>} [envOverrides] - optional env vars to overlay
|
||||
* on process.env. Pass { CI: '' } to suppress GitHub Actions CI detection
|
||||
* so that version-manager-pin is exercised even inside CI runners.
|
||||
*/
|
||||
function runScript(cwd, args = [], envOverrides = {}) {
|
||||
const result = spawnSync('bash', [SCRIPT, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
env: { ...process.env, ...envOverrides },
|
||||
});
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
stdout: result.stdout ?? '',
|
||||
stderr: result.stderr ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
describe('check-env.sh', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// Dynamic .nvmrc setup: write fixture .nvmrc files at test-run time so the
|
||||
// tests are correct across all Node major versions in the CI matrix (Node 22,
|
||||
// 24, 26, …). A hardcoded value like "26" passes on Node 26 but fails on
|
||||
// every other matrix row; using the active major makes the fixture portable.
|
||||
//
|
||||
// good/ → .nvmrc = active Node major (should match → exit 0)
|
||||
// bad-nvmrc/ → .nvmrc = active+99 (guaranteed mismatch → exit 1)
|
||||
// -------------------------------------------------------------------------
|
||||
const activeNodeMajor = parseInt(process.version.match(/^v(\d+)/)[1], 10);
|
||||
const goodNvmrc = path.join(FIXTURE_ROOT, 'good', '.nvmrc');
|
||||
const badNvmrc = path.join(FIXTURE_ROOT, 'bad-nvmrc', '.nvmrc');
|
||||
let originalGoodNvmrc;
|
||||
let originalBadNvmrc;
|
||||
|
||||
before(() => {
|
||||
originalGoodNvmrc = fs.existsSync(goodNvmrc) ? fs.readFileSync(goodNvmrc, 'utf8') : null;
|
||||
originalBadNvmrc = fs.existsSync(badNvmrc) ? fs.readFileSync(badNvmrc, 'utf8') : null;
|
||||
fs.writeFileSync(goodNvmrc, `${activeNodeMajor}\n`);
|
||||
fs.writeFileSync(badNvmrc, `${activeNodeMajor + 99}\n`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (originalGoodNvmrc !== null) {
|
||||
fs.writeFileSync(goodNvmrc, originalGoodNvmrc);
|
||||
}
|
||||
if (originalBadNvmrc !== null) {
|
||||
fs.writeFileSync(badNvmrc, originalBadNvmrc);
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 1: Happy path — all checks green
|
||||
// -------------------------------------------------------------------------
|
||||
test('exits 0 in a fixture directory with engines, .nvmrc, and matching lockfile', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'good');
|
||||
const { status, stdout } = runScript(cwd);
|
||||
assert.equal(
|
||||
status, 0,
|
||||
`Expected exit 0, got ${status}.\nstdout: ${stdout}`
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 2: engines.node constraint not satisfied
|
||||
// -------------------------------------------------------------------------
|
||||
test('exits 1 when engines.node constraint is not satisfied by current Node', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'bad-node-version');
|
||||
// Fixture has engines.node: "<14.0.0"; current Node is much higher.
|
||||
const { status, stdout } = runScript(cwd);
|
||||
assert.equal(
|
||||
status, 1,
|
||||
`Expected exit 1 (bad node version), got ${status}.\nstdout: ${stdout}`
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 3: Missing lockfile
|
||||
// -------------------------------------------------------------------------
|
||||
test('exits 1 when package-lock.json is missing', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'missing-lockfile');
|
||||
const { status, stdout } = runScript(cwd);
|
||||
assert.equal(
|
||||
status, 1,
|
||||
`Expected exit 1 (missing lockfile), got ${status}.\nstdout: ${stdout}`
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 4: .nvmrc major doesn't match active Node major
|
||||
// -------------------------------------------------------------------------
|
||||
test('exits 1 when .nvmrc major version does not match active Node major', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'bad-nvmrc');
|
||||
// Fixture .nvmrc is set to (activeNodeMajor + 99) by the before() hook above,
|
||||
// guaranteeing a mismatch regardless of the CI matrix Node version.
|
||||
// Override CI='' so the version-manager-pin check is not skipped even when
|
||||
// this test runs inside a CI runner (GitHub Actions sets CI=true, which
|
||||
// would otherwise turn the pin check into a skip and exit 0).
|
||||
const { status, stdout } = runScript(cwd, [], { CI: '' });
|
||||
assert.equal(
|
||||
status, 1,
|
||||
`Expected exit 1 (nvmrc mismatch), got ${status}.\nstdout: ${stdout}`
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 5: --json flag produces parseable JSON with documented shape
|
||||
// -------------------------------------------------------------------------
|
||||
test('--json emits parseable JSON with pass and checks keys', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'good');
|
||||
const { status, stdout } = runScript(cwd, ['--json']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch (err) {
|
||||
assert.fail(`--json output was not valid JSON: ${err.message}\nstdout: ${stdout}`);
|
||||
}
|
||||
// Top-level shape
|
||||
assert.equal(typeof parsed.pass, 'boolean', 'JSON must have boolean `pass` key');
|
||||
assert.ok(Array.isArray(parsed.checks), 'JSON must have array `checks` key');
|
||||
// The good fixture has engines.node, .nvmrc, and package-lock.json — expect
|
||||
// at least the node-version, lockfile-present, lockfile-sync, and
|
||||
// version-manager-pin checks to appear.
|
||||
const checkNames = parsed.checks.map((c) => c.name);
|
||||
assert.ok(
|
||||
checkNames.includes('node-version'),
|
||||
`Expected 'node-version' check in JSON, got: ${checkNames.join(', ')}`
|
||||
);
|
||||
assert.ok(
|
||||
checkNames.includes('lockfile-present'),
|
||||
`Expected 'lockfile-present' check in JSON, got: ${checkNames.join(', ')}`
|
||||
);
|
||||
// Every check item must have name, status, message fields with expected types
|
||||
for (const check of parsed.checks) {
|
||||
assert.equal(typeof check.name, 'string', `check.name must be string in ${JSON.stringify(check)}`);
|
||||
assert.ok(
|
||||
['pass', 'fail', 'skip'].includes(check.status),
|
||||
`check.status must be pass|fail|skip in ${JSON.stringify(check)}`
|
||||
);
|
||||
assert.equal(typeof check.message, 'string', `check.message must be string in ${JSON.stringify(check)}`);
|
||||
}
|
||||
// Good fixture: overall result must be pass:true
|
||||
assert.equal(parsed.pass, true, 'good fixture must report pass:true');
|
||||
assert.equal(
|
||||
status, 0,
|
||||
`Expected exit 0 in good fixture with --json, got ${status}`
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 5b: --json reports pass:false on failure fixtures (counter-test for 5)
|
||||
// -------------------------------------------------------------------------
|
||||
test('--json reports pass:false when a check fails', () => {
|
||||
const cwd = path.join(FIXTURE_ROOT, 'bad-node-version');
|
||||
const { status, stdout } = runScript(cwd, ['--json']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch (err) {
|
||||
assert.fail(`--json output was not valid JSON: ${err.message}\nstdout: ${stdout}`);
|
||||
}
|
||||
assert.equal(parsed.pass, false, 'failure fixture must report pass:false');
|
||||
assert.equal(status, 1, `Expected exit 1 with --json on failure fixture, got ${status}`);
|
||||
// The node-version check must be present and marked fail
|
||||
const nodeCheck = parsed.checks.find((c) => c.name === 'node-version');
|
||||
assert.ok(nodeCheck, 'node-version check must appear in JSON output');
|
||||
assert.equal(nodeCheck.status, 'fail', `Expected node-version status=fail, got ${nodeCheck.status}`);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 6: Integration smoke — script runs without tool-error on live root
|
||||
//
|
||||
// Verifies the script executes against a real repo without a tool error (exit 2).
|
||||
// Exit 0 or 1 are acceptable — local Node may differ from the .nvmrc pin (22).
|
||||
// Uses --json for structured assertion, avoiding raw output-grep.
|
||||
// -------------------------------------------------------------------------
|
||||
test('script runs without tool error on the live worktree root (--json)', () => {
|
||||
const { status, stdout, stderr } = runScript(LIVE_ROOT, ['--json']);
|
||||
assert.notEqual(
|
||||
status, 2,
|
||||
`Expected exit 0 or 1 on live repo, got exit 2 (tool error).\nstdout: ${stdout}\nstderr: ${stderr}`
|
||||
);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch (err) {
|
||||
assert.fail(`Live repo --json was not valid JSON: ${err.message}\nstdout: ${stdout}`);
|
||||
}
|
||||
assert.equal(typeof parsed.pass, 'boolean', 'Live repo JSON must have boolean pass');
|
||||
assert.ok(Array.isArray(parsed.checks), 'Live repo JSON must have checks array');
|
||||
// Node version check must be present and pass (Node >=22 is installed)
|
||||
const nodeCheck = parsed.checks.find((c) => c.name === 'node-version');
|
||||
assert.ok(nodeCheck, 'node-version check must be present in live repo output');
|
||||
assert.equal(nodeCheck.status, 'pass', `node-version should pass on live repo, got: ${nodeCheck.status} — ${nodeCheck.message}`);
|
||||
// Lockfile checks must pass on the live repo
|
||||
const lockfileCheck = parsed.checks.find((c) => c.name === 'lockfile-present');
|
||||
assert.ok(lockfileCheck, 'lockfile-present check must appear in live output');
|
||||
assert.equal(lockfileCheck.status, 'pass', `lockfile-present should pass on live repo`);
|
||||
});
|
||||
});
|
||||
@@ -153,6 +153,13 @@ const INTERNAL_COMPONENT_SLUGS = new Set([
|
||||
// docs/discussions/grok-build-support-2026-05.md. The regex captures
|
||||
// "/gsd-sync-skills" from the path. Invoked via Skill(skill="gsd-sync-skills").
|
||||
'sync-skills',
|
||||
|
||||
// gsd-test-runner — GitHub repository name: "github.com/open-gsd/gsd-test-runner".
|
||||
// docs/contributing/bootstrap.md references it as a hyperlink target:
|
||||
// [gsd-test-runner](https://github.com/open-gsd/gsd-test-runner)
|
||||
// The regex captures "/gsd-test-runner" from the URL path component. This is
|
||||
// an external tool repo, not a user-typable slash command in this product.
|
||||
'test-runner',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
15
tests/fixtures/check-env/bad-node-version/package-lock.json
generated
vendored
Normal file
15
tests/fixtures/check-env/bad-node-version/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "check-env-fixture-bad-node",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "check-env-fixture-bad-node",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "<14.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
tests/fixtures/check-env/bad-node-version/package.json
vendored
Normal file
7
tests/fixtures/check-env/bad-node-version/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "check-env-fixture-bad-node",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "<14.0.0"
|
||||
}
|
||||
}
|
||||
1
tests/fixtures/check-env/bad-nvmrc/.nvmrc
vendored
Normal file
1
tests/fixtures/check-env/bad-nvmrc/.nvmrc
vendored
Normal file
@@ -0,0 +1 @@
|
||||
22
|
||||
15
tests/fixtures/check-env/bad-nvmrc/package-lock.json
generated
vendored
Normal file
15
tests/fixtures/check-env/bad-nvmrc/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "check-env-fixture-bad-nvmrc",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "check-env-fixture-bad-nvmrc",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
tests/fixtures/check-env/bad-nvmrc/package.json
vendored
Normal file
7
tests/fixtures/check-env/bad-nvmrc/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "check-env-fixture-bad-nvmrc",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
1
tests/fixtures/check-env/good/.nvmrc
vendored
Normal file
1
tests/fixtures/check-env/good/.nvmrc
vendored
Normal file
@@ -0,0 +1 @@
|
||||
26
|
||||
16
tests/fixtures/check-env/good/package-lock.json
generated
vendored
Normal file
16
tests/fixtures/check-env/good/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "check-env-fixture-good",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "check-env-fixture-good",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/fixtures/check-env/good/package.json
vendored
Normal file
8
tests/fixtures/check-env/good/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "check-env-fixture-good",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
7
tests/fixtures/check-env/missing-lockfile/package.json
vendored
Normal file
7
tests/fixtures/check-env/missing-lockfile/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "check-env-fixture-missing-lockfile",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user