Skip to content

upsert_gitignore_baseline must preserve baseline negations + migrate unmarkered baselines (blocks #800 apply) #809

Closed

Description

@don-petry

Problem

upsert_gitignore_baseline in scripts/lib/gitignore-baseline.sh (shipped in #798) prepends the marker-wrapped L1 secrets baseline and keeps the target's existing content below the END marker as "L2". That is correct for a greenfield .gitignore, but the fleet dry-run for #800 proved it ships a correctness regression on repos that already contain secret patterns.

Verified failures (fleet dry-run, 2026-07-19)

Reproduced with git check-ignore on the upsert output of live repo .gitignores:

  • markets and google-app-scripts: after upsert, public.pem becomes IGNORED. Their ad-hoc .gitignore has a broad *.pem with no !public.pem; sitting below END as L2, it re-hides the file the L1 block re-allows via !public.pem. The baseline explicitly warns: "a later ignore silently re-hides a previously-negated file."
  • TalkTerm / bmad-bgreat-suite / ContentTwin: carry the old unmarkered baseline, so upsert duplicates the whole 398-line block (TalkTerm 434 → 831 lines; .env, *.pem twice).

This blocks the #800 fleet back-fill — applying as-is would gitignore legitimate public keys and produce 800-line files.

Required behavior

  1. Baseline negations must win. When the target's L2 content contains a broad ignore that would re-hide a baseline-negated path (!public.pem, !.env.example, !*.crt, !ca.crt, !*.pub, and the other ! re-allows in the block), neutralize it — strip the conflicting L2 line, or re-emit the baseline negations after L2 so they evaluate last.
  2. Migrate, don't duplicate. Detect an existing unmarkered baseline in the target and fold it into the single marker-wrapped block instead of prepending a second full copy. Genuine ecosystem/OS L2 entries are preserved.

Acceptance / required tests (bats)

  • markets-like input (*.pem, no !public.pem) → after upsert, in a temp git init dir, git check-ignore public.pem is NOT ignored (exit 1).
  • google-app-scripts-like input → same.
  • TalkTerm-like input (full unmarkered baseline) → exactly one BEGIN petry-projects marker; no duplicated .env / *.pem; genuine L2 preserved.
  • Existing Story: SEED + DISTRIBUTE — baseline into repo-template, bootstrap, and deploy sync #798 upsert tests still pass (idempotent, marker replace-in-place, negations survive verbatim).

Repro probe

source scripts/lib/gitignore-baseline.sh
BLOCK=$(mktemp); gib_extract_baseline_block ./.gitignore > "$BLOCK"
cur=$(mktemp); gh api repos/petry-projects/markets/contents/.gitignore --jq .content | base64 -d > "$cur"
d=$(mktemp -d); ( cd "$d" && git init -q )
upsert_gitignore_baseline "$BLOCK" "$cur" > "$d/.gitignore"
( cd "$d" && git check-ignore public.pem ) && echo "BUG: public.pem ignored" || echo "OK"

Blocks the #800 fleet apply. Part of Epic #801.

Activity

  1. don-petry commented on Jul 19, 2026

    @don-petry
    ContributorAuthor

    Dev-Lead Implementation Plan

    Issue: #809 — upsert_gitignore_baseline must preserve baseline negations + migrate unmarkered baselines (blocks #800 apply)

    Scope

    Make upsert_gitignore_baseline neutralize L2 content that conflicts with the L1 block by de-duplicating L2 lines against the block — a single mechanism that both lets baseline negations win (strips re-hiding broad patterns like a bare *.pem) and folds an existing unmarkered baseline into the one marker-wrapped block instead of duplicating it.

    Root cause

    L2 (everything below END) is preserved verbatim and evaluated after the block. A bare *.pem in L2 re-hides public.pem that the block re-allows via !public.pem; a full unmarkered old baseline in L2 gets duplicated on upsert.

    Implementation Checklist

    • scripts/lib/gitignore-baseline.sh: add _gib_neutralize_l2 helper that strips any L2 line (CR-stripped, trailing-ws-trimmed) that exactly matches a non-blank line of the block, and squeezes the resulting blank-line runs.
    • Apply the helper to L2 in the marker-less insert path (block on top, existing → L2).
    • Apply the helper to the post/L2 region in the replace-in-place path.
    • Skip emitting L2 entirely when it neutralizes to empty (keeps brand-new-style output; preserves idempotency).

    Tests to Write (test/scripts/lib/gitignore-baseline.bats)

    • markets-like L2 (*.pem, no !public.pem) → in a temp git init dir, git check-ignore public.pem exits 1 (NOT ignored).
    • google-app-scripts-like L2 → same.
    • TalkTerm-like L2 (full unmarkered baseline copy) → exactly one BEGIN marker, .env/*.pem appear once, genuine ecosystem L2 (node_modules/, *.log) preserved.
    • Negation-win + migration still idempotent on second upsert.
    • All existing Story: SEED + DISTRIBUTE — baseline into repo-template, bootstrap, and deploy sync #798 tests still pass unchanged.

    Test Command

    ./node_modules/.bin/bats test/scripts/lib/gitignore-baseline.bats (full gate: also test/scripts/sync-gitignore-baseline/ and tests/test_seed_repo_template_gitignore.bats), plus bash .dev-lead/scripts/dev-lead-lint.sh.

  2. don-petry commented on Jul 19, 2026

    @don-petry
    ContributorAuthor

    Dev-Lead: Implementation Complete

    Plan Execution

    • Added _gib_neutralize_l2 helper to scripts/lib/gitignore-baseline.sh
    • Wired it into the marker-less INSERT path and the marker-present REPLACE-in-place path
    • Skip emitting L2 when it neutralizes to empty (preserves brand-new-style output + idempotency)
    • Wrote the required bats: markets / google-app-scripts negation-win, TalkTerm migration, idempotency
    • Full gate suite + bootstrap/seed suites + shellcheck + dev-lead-lint all green

    Approach

    L2 (everything below END) is evaluated by git after the L1 block, so a bare *.pem
    kept in L2 re-hid the public.pem the block re-allows, and an old unmarkered baseline
    pasted into L2 got duplicated on upsert. Fix: _gib_neutralize_l2 drops any L2 line
    that exactly matches a pattern line of the block (comments/blank lines are ignored —
    the block's bare # separators must not clobber a repo's own comments). This single
    mechanism makes baseline negations win (the re-hiding duplicate is removed; the block
    still ignores real secrets and its !public.pem is no longer overridden) and folds a
    pasted unmarkered baseline into the one marker block. Genuine ecosystem/OS L2 is preserved;
    blank runs left behind are squeezed. Idempotent.

    Test Results

    1..23
    ok 1 gib_extract_baseline_block pulls the BEGIN..END block inclusive
    ok 2 gib_extract_baseline_block fails when no markers are present
    ok 3 gib_extract_baseline_block works on the real repo /.gitignore (anchors present)
    ok 4 absent: inserts the block on top, existing content becomes L2 below END
    ok 5 absent + empty existing: output is exactly the block
    ok 6 present: replaces a stale block between markers, leaving L2 intact
    ok 7 idempotent: upserting an already-upserted file is a no-op
    ok 8 negations survive verbatim and stay immediately after their broad pattern
    ok 9 replace does not drop a negation the stale block already carried
    ok 10 upsert fails on a file with only BEGIN marker (half-open state)
    ok 11 upsert fails on a file with only END marker (half-open state)
    ok 12 #809 markets-like: broad *.pem in L2 does not re-hide baseline-negated public.pem
    ok 13 #809 google-app-scripts-like: broad *.pem in L2 does not re-hide public.pem
    ok 14 #809 TalkTerm-like: unmarkered baseline in L2 is folded into one block, not duplicated
    ok 15 #809 negation-win + migration is idempotent
    ok 16 dry-run plans a PR to INSERT the baseline into a repo missing .gitignore
    ok 17 dry-run skips a repo whose .gitignore already carries the current baseline
    ok 18 dry-run plans a PR for a marker-less .gitignore (INSERT, L2 preserved)
    ok 19 errors when the canonical baseline is unreadable
    ok 20 template .gitignore leads with the marker-wrapped L1 block
    ok 21 template .gitignore passes the STORY3 anchor check (.env, *.pem, *.key present)
    ok 22 template .gitignore keeps its L2 ecosystem entries below the END marker
    ok 23 template .gitignore does not re-ignore a baseline-negated path (no bare .env.* in L2)
    

    Files Changed

    Notes

    • Only pattern lines are de-duplicated, never comments — this both avoids the block's bare # separators clobbering a repo's own comment lines and keeps the Story: SEED + DISTRIBUTE — baseline into repo-template, bootstrap, and deploy sync #798 "already carries the baseline → skip" sync path (which relies on exact idempotency) working. Comment residue from a migrated old baseline is left in place: harmless (comments don't affect git) and the functional duplication the issue flagged is fully removed.
    • Neutralization is applied in the marker-present REPLACE path too (not just the two failing cases), so requirement 1 holds regardless of marker state.
    • No git operations performed — CI handles commit/PR.
  3. added a commit that references this issue on Jul 19, 2026
  4. added a commit that references this issue on Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dev-leadFor dev-lead agent pickupenhancementFeature requests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions