Run before pushing any branch that adds shipped files, hooks, or platform-specific code. Prevents the CI-whiplash pattern — multiple red pushes that could have been caught locally.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add The-Artificer-of-Ciphers-LLC/skills-from-the-artificer --skill ci-preflight --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ci Preflight?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/the-artificer-of-ciphers-llc-ci-preflight)More formats (shields.io, HTML) on the badges page.
---
name: ci-preflight
description: Run before pushing any branch that adds shipped files, hooks, or platform-specific code. Prevents the CI-whiplash pattern — multiple red pushes that could have been caught locally.
---
# CI Preflight
> A green CI is not a reward for guessing correctly. It's the outcome of a local gate that already ran.
## The three CI-whiplash anti-patterns
This skill exists because these three failures happen repeatedly and all have the same root cause: pushing before checking.
---
### Anti-pattern 1: Missing registration surfaces
**What happens:** A new shipped file (hook, agent, command, module) passes unit tests but fails integration tests because it wasn't added to one of several whitelists/registries/inventories the repo maintains.
**Why it's hard:** These surfaces are spread across multiple files with no obvious connection — you find one and miss two others.
**Rule:** Before pushing any new file that ships to users, run a completeness search.
```bash
# Find every place "existing-hook.js" is referenced in the repo
grep -r "gsd-existing-hook" . --include="*.js" --include="*.cjs" --include="*.md" --include="*.json" -l
```
Use an existing sibling file as the probe: if `gsd-existing-hook.js` appears in N files, your new hook must appear in the same N files. Missing even one fails CI.
**In this repo, adding a new hook requires updating all of:**
- `hooks/managed-hooks-registry.cjs` — `MANAGED_HOOKS` array
- `scripts/build-hooks.js` — `HOOKS_TO_COPY` array
- `bin/install.js` — registration block with `fs.existsSync` guard
- `get-shit-done/bin/lib/installer-migration-report.cjs` — `BUNDLED_GSD_HOOK_FILES` Set
- `docs/INVENTORY.md` — headline count + table row
- `docs/INVENTORY-MANIFEST.json` — run `node scripts/gen-inventory-manifest.cjs --write`
**The pattern generalises:** every repo with drift-guard tests has its own set of registration surfaces. Find them before you write the first line.
---
### Anti-pattern 2: No local test run before pushing
**What happens:** Tests that would have caught the problem run in CI but not locally. Each failure costs a full CI round-trip (5–15 minutes) instead of a 30-second local run.
**Rule:** Run the repo's full test command before every push. Not a subset — the full suite.
```bash
npm test # or whatever this repo uses
npm run lint # catch lint errors before they hit CI
```
If the full suite is too slow for every push, run `npm test:affected` (or the repo's equivalent) — but know what it misses.
**⚠️ `npm test` does not always mirror CI's unit scope.** In gsd-core specifically, the default `npm test` (`node scripts/run-tests.cjs`) selects a different set than CI's unit job — observed in practice reporting `fail 0` locally while CI's unit job failed on `inventory-counts`. Mirror the CI unit job directly before pushing:
```bash
npm run test:unit # mirrors CI's "test (ubuntu, NN)" / "full test" unit scope; catches drift-guard tests npm test can skip
```
**Specific CI gates to run locally before pushing:**
```bash
npm run lint:changeset # catches missing/malformed changeset
npm run lint:docs # catches missing docs update
node scripts/gen-inventory-manifest.cjs # verify manifest is current
node --test tests/inventory-counts.test.cjs tests/inventory-manifest-sync.test.cjs tests/bug-3628*
```
**When you add a new shipped surface, run the drift-guard tests explicitly** — they're fast and catch exactly the "I forgot to update the registry" error.
---
### Anti-pattern 3: Guessing at cross-platform fixes
**What happens:** A test fails on a platform you can't run locally (Windows). You form a hypothesis, push a fix, it fails again. Repeat 3 times.
**Why it's expensive:** Each guess costs a full 10-minute Windows CI run. Three guesses = 30 minutes of CI time and a messy commit history.
**Rule:** Diagnose before fixing. Understand exactly what the platform does differently before writing a single line of code.
**Diagnosis checklist for cross-platform path failures:**
1. **Read the failure message literally.** "Path inside worktree should pass. stderr: (empty)" means the hook is exiting 2 (blocking) when it should pass. The path comparison is producing false negatives, not false positives.
2. **Identify the data sources.** List every path-valued variable and its origin:
- Test-constructed path (via `path.join`, `os.tmpdir`, `fs.realpathSync`)
- Git-returned path (`git rev-parse --show-toplevel` output)
- `path.resolve()` output
- `fs.realpathSync()` output
On Windows these can differ in: separator (`/` vs `\`), case (`Runner` vs `runner`), 8.3 short name vs long name.
3. **Find the mismatch.** On Windows, `os.tmpdir()` returns `C:\Users\RUNNER~1\AppData\Local\Temp` (8.3 short name). `fs.realpathSync` expands to the long name. `git --show-toplevel` returns whatever form the working tree was registered under. These three can all differ.
4. **Design for sameness, not normalisation.** The fix for "two path values that might not match" is usually to ensure they come from the **same source** rather than normalising disparate sources into a common form. In this PR: comparing two `git --show-toplevel` outputs (same binary, same format) worked; comparing `path.resolve` vs `realpathSync` didn't.
**The key insight:** Normalisation fights the platform. Sameness of origin avoids the fight entirely.
---
## Pre-push checklist
Run through this before every `git push` on a branch that changes shipped files:
```
[ ] npm run test:unit (mirrors CI's unit job — npm test alone can under-cover it; see Anti-pattern 2)
[ ] npm run lint
[ ] If adding a new shipped file:
[ ] Grepped for an existing sibling to find all registration surfaces
[ ] Added to every surface the sibling appears in
[ ] Ran drift-guard tests directly: node --test tests/inventory-*.test.cjs tests/bug-*whitelist*.test.cjs
[ ] If writing cross-platform path code:
[ ] Listed every path variable and its origin (test-constructed / git / path.resolve / realpathSync)
[ ] Identified which sources can diverge on Windows (8.3 names, case, separators)
[ ] Chosen "same source" over "normalise disparate sources"
[ ] No TODOs left in the diff that belong in CI
```
## Quick reference: common drift-guard test patterns in this repo
| Test file pattern | What it guards |
|---|---|
| `inventory-counts` | `docs/INVENTORY.md` headline numbers match filesystem counts |
| `inventory-manifest-sync` | `docs/INVENTORY-MANIFEST.json` matches filesystem |
| `bug-3628-bundled-hook-*` | `BUNDLED_GSD_HOOK_FILES` matches `hooks/` directory |
| `bug-1754-js-hook-guard` | Every `.js` hook has `fs.existsSync` guard in `install.js` |
| `managed-hooks` | `managed-hooks-registry.cjs` matches `hooks/` directory |
When you add a new hook, run all five before pushing.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!