Skip to content

fix(base-image): stop appending to .gitignore from startup.sh (#953)#978

Merged
vybe merged 1 commit into
devfrom
fix/953-gitignore-drift
Jun 1, 2026
Merged

fix(base-image): stop appending to .gitignore from startup.sh (#953)#978
vybe merged 1 commit into
devfrom
fix/953-gitignore-drift

Conversation

@dolho

@dolho dolho commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Freshly-deployed agents reported M .gitignore against origin/main immediately after deployment, with no user action. The drift came from two echo "<pattern>" >> /home/developer/.gitignore sites in `docker/base-image/startup.sh` whose grep -q guards used inconsistent / fragile patterns:

  • Site 1 (inside the GitHub-clone block): grep -q ".local/" — unanchored regex, fragile against the comment-only header written by the [ ! -f ] create-if-missing block right above it.
  • Site 2 (end of startup): grep -q "^content/$" / grep -q "^\.local/$" — anchored exact match, false-negative on trailing whitespace, comments, CRLF, or files missing terminal newline.

Result: even templates whose committed .gitignore already shipped the canonical patterns ended up with M .gitignore against origin/main.

Reproduction (verified locally)

mkdir -p /tmp/t953-fake && cd /tmp/t953-fake
git init -q
printf 'node_modules/\n*.log\n' > .gitignore
git add .gitignore && git commit -qm initial
git update-ref refs/remotes/origin/main HEAD

# Simulate the OLD startup.sh blocks:
grep -q ".local/" .gitignore || echo ".local/" >> .gitignore
grep -q "^content/$" .gitignore || echo "content/" >> .gitignore
grep -q "^\.local/$" .gitignore || echo ".local/" >> .gitignore

git status -s   # → " M .gitignore"
git diff origin/main   # → +.local/ +content/

After the fix (no >> .gitignore writes from startup.sh), the same script leaves the working tree clean.

Fix

Removed both append sites entirely. The canonical pattern list lives in _GITIGNORE_PATTERNS (src/backend/services/git_service.py) and is applied via _build_gitignore_merge_command's idempotent grep -qxF checks (fixed-string, exact-line — no anchor false-negatives). Two call sites cover the lifecycle:

  • initialize_git_in_container runs the merge on first Trinity-orchestrated git init.
  • _migrate_workspace_gitignore runs it on every push so existing agents adopt new patterns without rebuilding.

Trade-off

For the GitHub-clone-in-container flow, between container boot and first Trinity push, git status may show .local/-class files as untracked if the cloned template's .gitignore is incomplete. No remote impact — the canonical merge runs at the start of Trinity's push handler before any commit hits GitHub. Templates whose .gitignore ships the patterns correctly stop drifting (AC #4).

Existing agents are unaffected: their already-baked startup.sh keeps writing patterns until they pull a new base image, at which point the canonical merge takes over via the next push.

Tests

  • tests/unit/test_953_startup_sh_no_gitignore_writes.py — regression guard: greps startup.sh for any > /home/developer/.gitignore or >> redirect and fails if one is reintroduced. Also asserts the replacement comment keeps a pointer to the canonical helper.

Related to #953

Test plan

  • Reproduce drift with OLD shell logic in scratch git repo (M .gitignore + +.local/ +content/ diff)
  • Verify same scenario with NEW (empty) appender → clean git status
  • .venv/bin/pytest tests/unit/test_953_startup_sh_no_gitignore_writes.py — 2 passed
  • Rebuild trinity-agent-base:latest and deploy a fresh agent against a minimal template — confirm git status clean

🤖 Generated with Claude Code

`docker/base-image/startup.sh` had two sites that did
`echo "<pattern>" >> /home/developer/.gitignore` if a `grep -q` guard
missed — once inside the GitHub-clone block (for `.local/`) and once
unconditionally at end of startup (for `content/` + `.local/`).

The greps used inconsistent patterns:
- `grep -q ".local/"` — unanchored regex; `.` matches any char so it
  almost always succeeds, but on first boot of a fresh clone the file
  may not exist yet and the prior `[ ! -f ]` block writes a comment-
  only header that the grep then matches against.
- `grep -q "^content/$"` / `grep -q "^\.local/$"` — anchored exact
  match; misses on trailing whitespace, comments, CRLF, or a file
  missing its terminal newline. Field hypothesis (issue body) is the
  trailing-whitespace / missing-newline path.

Result: any template whose `.gitignore` already (correctly) contained
the canonical patterns could still end up with `M .gitignore` against
`origin/main` immediately after deployment — verified by reproducing
in a scratch git repo: with a 2-line `.gitignore` lacking
`.local/`/`content/`, simulating the startup.sh block produces `M
.gitignore` and a `+.local/\n+content/` diff against `origin/main`.

Fix: remove both append sites entirely.

The canonical pattern list lives in `_GITIGNORE_PATTERNS`
(`src/backend/services/git_service.py`) and is applied via
`_build_gitignore_merge_command`'s idempotent `grep -qxF` checks
(fixed-string, exact-line — no anchor false negatives). Two call
sites cover the lifecycle:
- `initialize_git_in_container` runs the merge on first
  Trinity-orchestrated git init.
- `_migrate_workspace_gitignore` runs it on every push so existing
  agents adopt new patterns without rebuilding.

Trade-off for the GitHub-clone-in-container flow: between container
boot and first Trinity push, `git status` may show `.local/`-class
files as untracked if the cloned template's `.gitignore` is
incomplete. No remote impact — the canonical merge runs before any
commit hits GitHub via Trinity's push handler. Templates whose
`.gitignore` ships the patterns correctly stop drifting from
`origin/main` (the AC's primary concern).

Regression guard:
`tests/unit/test_953_startup_sh_no_gitignore_writes.py` greps
startup.sh for any `> /home/developer/.gitignore` or `>>` redirect
and fails if one is reintroduced. Also asserts the replacement
comment block keeps a pointer to the canonical helper so future
readers can find it.

Existing agents are unaffected: their already-baked `startup.sh`
keeps writing patterns until they pull a new base image, at which
point the canonical merge takes over via the next push.

Related to #953

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dolho
dolho requested a review from vybe May 29, 2026 14:41

@vybe vybe 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.

LGTM — verified _GITIGNORE_PATTERNS (git_service.py:677,682) covers both removed shell patterns (.local/, content/) and the canonical merge uses grep -qxF (exact-line, no false negatives). CI fully green, regression test pins the fix. Validated via /validate-pr.

@vybe
vybe merged commit 5b9450e into dev Jun 1, 2026
14 checks passed
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.

2 participants