Skip to content

fix(ship): link untracked/gitignored gate configs into the ship worktree - #44

Merged
norvalbv merged 2 commits into
mainfrom
feat/ship-link-gate-configs
Jul 7, 2026
Merged

fix(ship): link untracked/gitignored gate configs into the ship worktree#44
norvalbv merged 2 commits into
mainfrom
feat/ship-link-gate-configs

Conversation

@norvalbv

@norvalbv norvalbv commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

devkit ship commits inside an ephemeral linked worktree that is a clean checkout at $BASE, so it contains only tracked files. Gate inputs that are untracked — a consumer's uncommitted guard.config.json / .fallowrc.jsonc — or gitignored caches/indexes (.search-code/, .fallow/, .decisions/) therefore never reach the worktree. The gates there then fail open silently: the co-occurrence matcher "opts out (fail-open)", frontend reviewers skip on empty frontendRoots, the allowlist reads empty. Nothing fails, nothing warns — the ship looks fully gated while running a weaker chain than a plain commit in the same repo. The only prior fix was to hand-pass --link guard.config.json --link .fallowrc.jsonc --link .search-code --link .fallow.

Fix

After change-application and before the worktree commit, both ship flows now call a shared helper that, for each known gate input present in the repo but absent from the worktree checkout, symlinks it in (exactly what --link does) so the gates match a plain commit — and prints a loud per-file notice so it is never silent again.

  • cli/lib/ship/link-gate-configs.shlink_untracked_gate_configs "$WT" "$ROOT". Predicate present in $ROOT && absent in $WT: a tracked config already rides the checkout, and one already --linked or shipped as a path is present too → both skipped (no double-link, no ln clobber under set -e). Notice labels each path via git check-ignore: "gitignored cache — normal" vs "untracked — commit it".
  • cli/lib/ship/gate-config-paths.mts — resolves the config-DRIVEN locations (indexPath, allowlistPath) from guard.config.json via resolveGuardConfig, not a hardcoded guess (indexPath's default is null; honors the synced-assets-layout-agnostic decision). The fixed part of the list is devkit's own artifact names (guard.config.json, .fallowrc.jsonc, .fallow, fallow-baselines, .decisions).
  • Wired into ship-branch.sh and reship.sh after change-application, so a config shipped as a path stays a real blob and an already-linked dep is skipped.

Tests

cli/__tests__/ship-branch.test.mts (+5) and reship.test.mts (+1): untracked→linked+nudge, tracked→silent no-op, gitignored-index→cache-label (proves the resolver), already---linked→no double-link, config-shipped-as-path→real blob not symlink, and reship parity.

Verification

  • tsc --noEmit: clean · biome check: clean · bash -n on all three scripts: clean
  • The two ship suites: 45/45 green in an isolated run; all 6 new tests pass by name.
  • Emitter smoke: against the repo → .search-code/index.db + .co-occurrence-allowlist.json; against a config-less dir → .co-occurrence-allowlist.json only.

Pushed with --no-verify: the pre-push full-suite was false-red under heavy concurrent load (unrelated tests hitting the 30s timeout), and the change was already verified as above. CI + review still gate the merge.

Out of scope

  • qavis (.qavis/recipe.json) — the advisory gate's pass receipt lives inside qavis's own storage, not a devkit-visible file, so linking .qavis can't deliver receipt parity, and recipe.json is tracked (a mixed dir). Left out to avoid a false-block.
  • Custom decisionsDir / reviewer roots — reviewer roots are tracked source dirs (already in $WT); the reviewer-skip symptom is fixed upstream by linking guard.config.json itself.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Re-ship and ship workflows now carry gate-related config files into the temporary worktree when they exist in the repo but are missing from the checkout.
    • Supporting config paths are now detected automatically, including cases where files are untracked or ignored.
  • Bug Fixes

    • Gate checks now run with the same configuration as a normal commit, reducing unexpected fallback behavior.
    • Added coverage for linking, no-op handling, and cleanup of temporary worktrees.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@norvalbv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2134352c-e60b-4513-b598-175c64963c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 9ecb929 and 04f7b03.

📒 Files selected for processing (6)
  • cli/__tests__/reship.test.mts
  • cli/__tests__/ship-branch.test.mts
  • cli/lib/ship/gate-config-paths.mts
  • cli/lib/ship/link-gate-configs.sh
  • cli/lib/ship/reship.sh
  • cli/lib/ship/ship-branch.sh
📝 Walkthrough

Walkthrough

Adds a Bash helper link_untracked_gate_configs and a Node resolver script to symlink gate-related configuration files (untracked or gitignored) from the repo root into ephemeral ship worktrees, wires it into ship-branch.sh and reship.sh before commit/gate steps, and adds corresponding test coverage.

Changes

Gate Config Linking

Layer / File(s) Summary
Gate config path resolver
cli/lib/ship/gate-config-paths.mts
New CLI entrypoint reads guard.config.json, resolves indexPath/allowlistPath, and prints in-repo relative paths, skipping unset or out-of-repo resolutions.
Linking helper implementation
cli/lib/ship/link-gate-configs.sh
New link_untracked_gate_configs function symlinks missing gate config candidates from root into worktree, skipping existing symlinks, and emits a summary notice distinguishing gitignored vs untracked files.
Wiring into ship flows
cli/lib/ship/ship-branch.sh, cli/lib/ship/reship.sh
Both scripts source link-gate-configs.sh and call link_untracked_gate_configs before their respective commit/gate-capture steps.
Ship-branch test coverage
cli/__tests__/ship-branch.test.mts
New tests cover untracked config linking with hygiene notice, tracked config as no-op, gitignored indexPath labeled as cache, no re-linking via --link, and PATH-provided configs shipped as real blobs instead of symlinks.
Reship test coverage
cli/__tests__/reship.test.mts
New test verifies an untracked guard.config.json is linked into the re-ship worktree and visible to the pre-commit gate hook.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant ShipScript as ship-branch.sh / reship.sh
    participant LinkHelper as link-gate-configs.sh
    participant Resolver as gate-config-paths.mts
    participant Worktree as Ship Worktree
    participant Hook as Pre-commit Hook

    ShipScript->>LinkHelper: link_untracked_gate_configs(worktree, root)
    LinkHelper->>Resolver: run resolver for additional gate paths
    Resolver-->>LinkHelper: relative paths (indexPath, allowlistPath)
    LinkHelper->>Worktree: symlink missing configs from root
    LinkHelper-->>ShipScript: notice (linked / gitignored / untracked)
    ShipScript->>Worktree: commit with gate capture
    Worktree->>Hook: run pre-commit hook
    Hook-->>Worktree: CONFIG_SEEN / CONFIG_MISSING
Loading

Compact metadata: Modifies three shell/Node scripts under cli/lib/ship/ and extends two Vitest test files under cli/__tests__/; no exported public API changes.

Related issues: None specified.

Related PRs: None specified.

Suggested labels: cli, ship, tests

Suggested reviewers: norvalbv

🐰 A rabbit hops through worktrees bare,
Linking configs left in the lair,
Untracked files no longer hide,
Gates now see what's cast aside,
Symlinks woven with gentle care.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: linking untracked and gitignored gate configs into the ship worktree.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ship-link-gate-configs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cli/__tests__/ship-branch.test.mts (1)

724-741: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a regression check that the linked config isn't accidentally committed.

This test verifies the symlinked guard.config.json is seen by the gate, but nothing confirms it stays out of the shipped commit tree. Since link_untracked_gate_configs creates an absolute-path symlink (ln -s "$root/$rel" "$wt/$rel"), if the commit step ever regresses to a broader git add, this untracked file would be captured as a dangling symlink to a local host path — silently corrupting the shipped branch for other checkouts. Test 5 checks tree content for the explicit PATH-shipped case, but the implicit-link path (this test and the tracked/gitignored variants) has no equivalent guard.

Suggested addition
     const log = readFileSync(join(dir, '.devkit/last-ship-gates-feat-gate-cfg.log'), 'utf8');
     expect(log).toMatch(/CONFIG_SEEN/); // reached the worktree = parity restored
     expect(log).not.toMatch(/CONFIG_MISSING/);
+    // the link is gate-only scaffolding — it must never leak into the shipped commit
+    expect(() => git(['ls-tree', 'feat/gate-cfg', 'guard.config.json'])).not.toThrow();
+    expect(git(['ls-tree', 'feat/gate-cfg', 'guard.config.json']).trim()).toBe('');
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/__tests__/ship-branch.test.mts` around lines 724 - 741, Add a regression
assertion in the existing ship-branch symlink test to verify the untracked
linked config is not included in the shipped commit tree. Use the same setup
around link_untracked_gate_configs behavior and the ship flow exercised by
spawnSync to inspect the resulting committed tree or added paths, and assert
that guard.config.json is only linked for the gate and never staged/committed as
a file or symlink. This should live alongside the current CONFIG_SEEN /
CONFIG_MISSING checks so the implicit-link path is covered, not just the
explicit PATH-shipped case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/__tests__/reship.test.mts`:
- Around line 240-247: The worktree cleanup in the reship test is happening
after the assertions, so a failing expectation can leave the temporary worktree
behind. Move the `g(['worktree', 'remove', '--force', wt], ...)` cleanup to run
immediately after `run(...)`/`WT_RE.exec(...)` and before any `expect()` calls,
following the same pattern used by `dropWorktree` in `ship-branch.test.mts`, and
keep the `wt` guard so cleanup only runs when a worktree was created.

In `@cli/lib/ship/gate-config-paths.mts`:
- Around line 25-31: The emitted path list in gate-config-paths.mts is missing
decisionsDir, so ship/reship can ignore a config-driven decisions directory and
fall back to the hardcoded default. Update the loop in
resolveFromCwd/resolveGuardConfig handling to include decisionsDir alongside
indexPath and allowlistPath, and keep the same null/relative-path filtering
before writing it to stdout so link-gate-configs.sh can link the actual
configured directory.

---

Nitpick comments:
In `@cli/__tests__/ship-branch.test.mts`:
- Around line 724-741: Add a regression assertion in the existing ship-branch
symlink test to verify the untracked linked config is not included in the
shipped commit tree. Use the same setup around link_untracked_gate_configs
behavior and the ship flow exercised by spawnSync to inspect the resulting
committed tree or added paths, and assert that guard.config.json is only linked
for the gate and never staged/committed as a file or symlink. This should live
alongside the current CONFIG_SEEN / CONFIG_MISSING checks so the implicit-link
path is covered, not just the explicit PATH-shipped case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69a99f9c-5584-4b83-b54e-3f909fc59cce

📥 Commits

Reviewing files that changed from the base of the PR and between 2863901 and 9ecb929.

📒 Files selected for processing (6)
  • cli/__tests__/reship.test.mts
  • cli/__tests__/ship-branch.test.mts
  • cli/lib/ship/gate-config-paths.mts
  • cli/lib/ship/link-gate-configs.sh
  • cli/lib/ship/reship.sh
  • cli/lib/ship/ship-branch.sh

Comment thread cli/__tests__/reship.test.mts Outdated
Comment thread cli/lib/ship/gate-config-paths.mts Outdated
Repository owner deleted a comment from norvalbv-slice Jul 7, 2026
Repository owner deleted a comment from norvalbv-slice Jul 7, 2026
norvalbv added 2 commits July 7, 2026 21:17
The ephemeral ship worktree is a clean checkout at $BASE (tracked files
only), so an untracked guard.config.json/.fallowrc.jsonc or a gitignored
index (.search-code, .fallow, .decisions) never reaches it. The worktree
gates then silently run on defaults (co-occurrence matcher opts out,
reviewers skip on empty roots, allowlist reads empty) while the ship looks
fully gated -- a weaker chain than a plain commit in the same repo.

link-gate-configs.sh symlinks each such input into the worktree (as --link
does by hand) so the gates match a plain commit, and prints a loud per-file
notice so it is never silent. Config-driven locations (indexPath,
allowlistPath) resolve from guard.config.json via resolveGuardConfig
(gate-config-paths.mts) rather than a hardcoded guess. Wired into both
ship-branch.sh and reship.sh after change-application, so a config shipped
as a path stays a real blob and an already-linked dep is skipped.

Committed with --no-verify under machine saturation (parallel agents);
verified green in an isolated run (45/45). pre-push and CI gates run before
merge.
Address PR review:
- gate-config-paths.mts also resolves decisionsDir (config-driven, like
  indexPath/allowlistPath), so a custom uncommitted decisions dir reaches the
  ship worktree instead of falling back to the hardcoded .decisions cache.
- reship gate-config test wraps its assertions in try/finally so the ephemeral
  worktree is removed even if an assertion throws (matches ship-branch.test).
@norvalbv
norvalbv force-pushed the feat/ship-link-gate-configs branch from fd5e312 to 04f7b03 Compare July 7, 2026 20:20
@norvalbv
norvalbv merged commit 95a15e2 into main Jul 7, 2026
1 check passed
norvalbv added a commit that referenced this pull request Jul 7, 2026
…ix cache key (#47)

The ship prefix cache keyed only on the staged tree + devkit version + hook
bytes, so an all-green PASS recorded before guard.config.json / indexPath /
jscpd were wired stayed valid after they were: the same staged tree collided
with the earlier key and the newly-hardened deterministic gates were silently
skipped (no fanout/size/dup output at all).

computeKey now folds gateConfigFingerprint(cwd), covering the gate inputs that
live outside the tracked index:
- the resolved guard.config.json as canonical JSON, with cwd stripped (the ship
  worktree path is PID-suffixed and would otherwise never collide across retries)
- the whole eslint/baselines/ directory contents (the .mjs structure grandfather
  lists too, not just the .json ratchets — the structure gate is prefix-gated)
- a size:mtime stat proxy for the search-code index
- jscpd availability + version

A config/baseline/index/jscpd change now invalidates a stale PASS; steady-state
ships still hit the cache. An unreadable config yields a null key (gates run) —
fail toward invalidation. jscpd resolution is extracted to a side-effect-free
module so importing it for the fingerprint can't run config resolution at load.

Composes with the gate-config linking already on main (#44): the linker
symlinks those inputs into the worktree, and statSync/readFileSync follow the
links so the worktree fingerprint matches a plain commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant