Skip to content

fix(push): publish the env files env add leaves in a standalone clone (#881) - #885

Merged
jeff-r2026 merged 5 commits into
Tencent:mainfrom
SaulMoro:fix/881-push-dirty-env
Sep 29, 2026
Merged

jeff-r2026 merged 5 commits into
Tencent:mainfrom
SaulMoro:fix/881-push-dirty-env

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

env add then push works again in a standalone clone: push keeps the env files env add left instead of refusing them, and publishes only the ones you select.

 push (standalone clone)
+  pending = dirty files EnvHandler.scanLocalForPush lists   # bytes + permission bits kept, git mode unchanged,
+                                                            # no staged/conflicted/deleted/renamed entry
-  unsafe  = dirty − {sync-lock, teamai.yaml}
+  unsafe  = dirty − {sync-lock, teamai.yaml} − pending
-  resetToCleanMaster; pullRepo
+  try { resetToCleanMaster; pullRepo } finally { write pending back }
   for each group
-    pushGroup                                               # sweeps env/ → commit
+    pushGroup                                               # stages the group's own env files only
+    pushed | pr-failed → pending −= group's env files
+    failed             → checkout default; write teamai.yaml (if still pending)
+    any outcome        → write pending back (bytes, then chmod)   # a no-change or PR-retry group resets the clone
+    a write fails      → name the file and the next step; stop (state saved)

The exempt set comes from scanLocalForPush, so it is exactly the env entry files (env/env.yaml, env/<namespace>/env.yaml) push would list. Still refused: any other dirty path, a deleted env.yaml, a mode change, and an env file with index state (e.g. staged, then edited again).

env/ is no longer swept: each env item is already its own path in the commit, and the sweep published env edits left out of the selection.

A restore that cannot write a file stops the push with Could not put back env/… (reason) … Run \teamai env add` again for the variables it held, then push.instead of being reported asPull failed` and carrying on.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

npm run test:e2e: 368 passed, 26 skipped.

Test main This PR
pushes the env.yaml edit ✖ refused ✔ on the pushed branch
another dirty path still refuses (Paths: README.md only) ✖ lists env/env.yaml too ✔
pull fails after reset → edit survives — ✔ (finally)
staged, then edited env.yaml still refuses; index and edit survive ✔ ✔
group rollback (--branch names an existing local branch) → edit and its 0600 mode survive — ✔
select env.yaml, deselect team/env.yaml → only the selected file is pushed; the other stays dirty — ✔
git push rejected after the commit → clone back on main with the edit dirty; retry publishes it — ✔
new env/ops/env.yaml from env add --role ops → restored, still 0600, after the rollback's clean -fd and after a rejected push — ✔
Latin-1 env.yaml hand edit → same bytes after the rollback — ✔
deselected env edit, reuse group with no PR whose diff is metadata-only (resets, retries the PR, reports pushed) → edit survives — ✔
deselected env edit, new group with a metadata-only diff (nochange) → edit survives — ✔
deselected env edit, --branch with only reuse groups, config-only push with no change → edit survives — ✔
teamai.yaml edit in the config group, git push rejected after the commit → kept on main ✖ left on the local branch ✔
env restore write fails after the refresh / after a group → push stops naming the file; no Pull failed; state saved ✖ swallowed / uncaught ✔
deleted / mode-changed env.yaml still refuses ✔ ✔

Each row added after review fails against the commit before its fix (8129a9e for the staged and rollback rows, 42c4821 for the selection and rejected-push rows, bf5098b for the teamai.yaml and restore-failure rows, f465062 for the mode, Latin-1 and reuse-retry rows). The nochange and config-only rows pass on f465062 too; they pin restores that were untested, and each fails when its restore is removed. The --role row passes on bf5098b too: the writeFile helper already recreates the directory, and the tests pin that (they fail with ENOENT under a bare fs.writeFile). The restore-failure row injects the write error through a mocked writeFile; it has no real-CLI record.

Real CLI, sandbox HOME, CLAUDE_CONFIG_DIR unset, generic-git fixture:

BEFORE  $ teamai env add TEAM_VAR changed && teamai push --all
        ✖ Cannot push: the team repo has uncommitted changes. … Paths: env/env.yaml

AFTER   $ teamai env add TEAM_VAR changed && teamai push --all
        ✔ Pushed branch teamai/push/t/20260928-151349
        branch:env/env.yaml → TEAM_VAR: changed

Review fixes, same fixture, commit before the fix vs this head:

staged, then edited ($ git status → MM env/env.yaml; teamai push --all)
  8129a9ef  ✔ Pushed branch …      index → value: first   worktree → value: first
  head      ✖ Cannot push: … Paths: env/env.yaml
                                   index → value: staged  worktree → value: changed   push branches: 0

rollback ($ teamai env add TEAM_VAR changed; git branch teamai/taken; teamai push --all --branch teamai/taken)
  8129a9ef  ✖ Push failed: fatal: a branch named 'teamai/taken' already exists   env.yaml → value: first
  head      ✖ Push failed: fatal: a branch named 'teamai/taken' already exists   env.yaml → value: changed  ( M)

deselect ($ teamai push, answer 1 of: 1. [env] env.yaml, 2. [env] team/env.yaml)
  42c4821b  ✔ Pushed branch …   branch: env.yaml → selected, team/env.yaml → deselected
                                clone team/env.yaml → value: first   status: clean
  head      ✔ Pushed branch …   branch: env.yaml → selected, team/env.yaml → first
                                clone team/env.yaml → value: deselected   status: M env/team/env.yaml

rejected push ($ teamai env add TEAM_VAR changed; remote pre-receive exits 1; teamai push --all; remove hook; teamai push --all)
  42c4821b  ✖ Push failed   clone HEAD → teamai/push/t/…  status: clean
            retry: ℹ No new or modified resources to push   remote branches: none
  head      ✖ Push failed   clone HEAD → main   status: M env/env.yaml
            retry: ✔ Pushed branch …   branch:env/env.yaml → value: changed

Round 3, same fixture, bf5098b vs this head:

--role, rollback ($ teamai env add OPS_VAR ops-value --role ops → ?? env/ops/env.yaml; git branch teamai/taken;
                  teamai push --all --branch teamai/taken)
  bf5098bc  ✖ Push failed: … 'teamai/taken' already exists   env/ops/env.yaml → value: ops-value  (??)
  head      ✖ Push failed: … 'teamai/taken' already exists   env/ops/env.yaml → value: ops-value  (??)

--role + teamai.yaml edit, rejected push ($ env add … --role ops; echo '# local edit' >> teamai.yaml;
                  remote pre-receive exits 1; teamai push --all; remove hook; teamai push --all)
  bf5098bc  ✖ Push failed   HEAD → main   env/ops/env.yaml → ops-value   teamai.yaml edit → gone
            retry: ✔ Pushed branch …   env/ops/env.yaml → ops-value   teamai.yaml edit → not on the branch
  head      ✖ Push failed   HEAD → main   env/ops/env.yaml → ops-value   teamai.yaml edit → kept ( M)
            retry: ✔ Pushed branch …   env/ops/env.yaml → ops-value   teamai.yaml edit → on the branch

Adversarial round, same fixture, umask 022, f465062 vs this head:

chmod 600, rollback ($ env add OPS_VAR secret --role ops; env add TEAM_VAR secret;
                     chmod 600 env/ops/env.yaml env/env.yaml; git branch teamai/taken; teamai push --all --branch teamai/taken)
  f465062e  ✖ Push failed: … 'teamai/taken' already exists   env/ops/env.yaml 644   env/env.yaml 644
  head      ✖ Push failed: … 'teamai/taken' already exists   env/ops/env.yaml 600   env/env.yaml 600

Latin-1 env.yaml (value: caf\xe9), same rollback
  f465062e  sha1 96bbb4d438d4 → c43d2cd2c29d   (0xE9 → U+FFFD)
  head      sha1 96bbb4d438d4 → 96bbb4d438d4

(The exit 1 on successful generic-git pushes is generic git's "no PR support", as today.)

Related Issues

Closes #881. Regressed in #690.

Notes for Reviewers

Door: two-way. Guarded blocks in pushCore and one sweeper entry in pushGroup; revert restores the refusal.

Blast Radius: push. The capture and restore run in standalone clones only. Dropping the env/ sweep reaches single-repo mode too, where the knowledge worktree only ever held the env files pushItem copied for the selection, so staging them by path gives the same commit (single-repo tests pass unchanged).

  • Deviates from [bug] push refuses the env files env add leaves in a standalone clone #881's sketch on purpose: writing back after pullRepo loses the edit when the pull fails after reset --hard. The finally keeps it; a test covers it.
  • The restore after each group mirrors the pendingTeamConfig re-apply ([bug] teamai push --branch: team-config edits attach to an existing PR group instead of the new branch #800): it runs whatever the outcome, because a reuse group retrying its PR resets the clone and still reports pushed. A pushed group first removes only its own env files from the pending set, since its branch carries exactly those. Rewriting a file still in place changes nothing.
  • A pushed group's own env file whose diff was metadata-only (a blank line) is dropped from the pending set after that reset, so the blank line is lost (from reading the code, not tested); pushRepoBranch treats such a diff as no change for every resource.
  • teamai.yaml keeps its text round-trip and does not keep a hand-set 0600 across reset --hard (already on main, fix(push): honor explicit branches and protect dirty team clones #690); left for the separate teamai.yaml follow-up.
  • After a failed group the local push branch is left in place, as today; the edits (env and a not-yet-pushed teamai.yaml) are back on the default branch, so the next run retries them under a new branch.
  • Same class, left alone, already on main: pushTeamConfigOnly (a teamai.yaml-only push) whose git push fails after the commit switches back to the default branch and leaves the edit on the local branch (from reading the code, not tested). It touches no env file.
  • Same trade-off as teamai.yaml today: a teammate's upstream change to the same env.yaml between env add and push is overwritten on the pushed branch. env add pulls first, so the window is small.
  • Left alone, already on main: a modified env.yaml is listed as "(new)".

…Tencent#881)

The Tencent#690 dirty-clone guard exempted only teamai.yaml and the sync lock, so
`env add` then `push` in a standalone clone stopped at "Cannot push: the
team repo has uncommitted changes" and the env edit was never published.

Capture the dirty files EnvHandler.scanLocalForPush lists (content readable,
mode unchanged), exempt them from the guard, and write them back after the
reset and pull. The write-back runs in a finally: unlike teamai.yaml nothing
later in the run holds the content, so a failed refresh after reset --hard
would otherwise drop the edit. Deletions, mode changes and every other dirty
path still stop the push.
@jeff-r2026 jeff-r2026 self-assigned this Sep 28, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/push.ts:917 exempts a path regardless of which Git status buckets contain it. If env/env.yaml is staged and then edited again, scanLocalForPush includes it because of the unstaged edit, so both its staged and modified entries are filtered out. reset --hard then silently destroys the staged state instead of refusing it as the PR claims. Only exempt env paths that have no staged/conflicted/deleted/renamed status.
  • [P1 blocking] src/push.ts:933 retains the env snapshot only through the initial refresh. If a user deselects the env item, selects another resource, and that group hits pushRepoBranch’s no-change/error cleanup, the later reset --hard/clean -fd in pushGroup silently deletes the still-pending env edit. Preserve and reapply unselected env snapshots anywhere subsequent cleanup can reset the clone, as already done for pendingTeamConfig.

The PR description includes a representative real-CLI run and sufficient test documentation.

Tencent#881)

Two review findings on the env exemption from the dirty-clone guard:

- A staged env file that was edited again was exempt through its unstaged
  edit, so reset --hard dropped the staged state. Only a working-copy edit
  with no staged, conflicted, deleted or renamed entry is captured now; any
  index state keeps the path unsafe and stops the push.
- The captured edits went back only after the initial refresh. A group that
  committed nothing (the pushGroup rollback, pushRepoBranch's no-change
  path, or the config-only push after reuse groups) reset the clone and lost
  them. They are restored after each such group, as teamai.yaml is, and
  dropped once a group's branch carries them through the env/ sweeper.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/push.ts:936 exempts every scanned env path before item selection. If a user deselects an env change but selects a skill or rule, pushGroup still stages the entire env/ directory, publishing the explicitly deselected env contents in that resource’s PR. Only exempt/stage env files that belong to the selected group.
  • [P1 blocking] src/push.ts:1683 does not preserve edits when git push fails after the local commit is created. The restore matches the feature-branch commit, leaving a clean status; the next teamai push captures nothing and switches back to main, so the env edit is no longer retried and is only recoverable from the stale local branch.

The two findings from the earlier review are resolved. The PR description includes sufficient real-CLI and automated test documentation.

…push committed (Tencent#881)

Two more review findings on the env exemption:

- pushGroup swept the whole env/ directory, so an env edit left out of the
  selection was published in whichever group pushed first. Each env item is
  already its own path in pushedFiles, so env/ is no longer swept, and a
  pushed group drops only its own files from the pending set. The deselected
  edits stay dirty in the clone.
- A git push that failed after the local commit left the clone on that
  branch, where the restore matched HEAD and left nothing dirty; the next
  push found no change and switched away. After a failed group the clone now
  goes back to the default branch before the pending edits are restored, so
  the next push retries them.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/push.ts:878 restores snapshots with writeFile without recreating parent directories. If teamai env add --role ops creates a new untracked env/ops/env.yaml and the push fails, pushGroup runs git clean -fd, deleting env/ops; restoration then throws ENOENT and the env edit is lost. Ensure the parent directory exists before writing each snapshot.

Both findings from earlier reviews are resolved. The PR description includes sufficient automated and representative real-CLI testing.

…oup's config edit (Tencent#881)

From an audit of the env capture/restore path:

- A restore write that failed was not reported. After the refresh it was
  caught as "Pull failed" and the push carried on without the edit; after a
  group it escaped push as a bare error before earlier groups' PR records
  were saved. The restore now returns the files it could not write, and push
  stops naming them and the next step, saving state first in the group loop.
- A config group whose git push failed after the commit went back to the
  default branch for its env edits and left the teamai.yaml edit behind on
  the local branch. The captured config is re-applied there too while no
  earlier group has pushed it.

A new env/<role>/env.yaml that the rollback's clean -fd removes already
comes back: the writeFile helper recreates the directory. Tests now cover it
for the rollback and for a rejected push.
@github-actions

Copy link
Copy Markdown

Review

  • No new findings in the current diff.
  • All previously raised findings are resolved.
  • The PR description includes sufficient automated tests and a representative real-CLI end-to-end verification.

…ry group (Tencent#881)

From the adversarial review:

- The snapshot held decoded UTF-8 text and no mode. A new env/<role>/env.yaml
  set to 0600 came back 0644 after clean -fd (git records no mode for an
  untracked file, and reset --hard recreates a tracked one under the umask
  too), and a hand edit that is not UTF-8 came back re-encoded. It now keeps
  the bytes and the permission bits and restores both.
- The restore ran only after a failed or no-change group. A reuse group
  retrying its missing PR takes pushRepoBranch's metadata-only reset path and
  still reports pushed, so a pending env edit was wiped with no restore. The
  restore now runs after every group, once a pushed group's own files are
  dropped from the pending set.
- The pushItem and filterExistingTopLevelPaths comments no longer describe
  an env/ sweeper.

Tests cover both modes, the non-UTF-8 bytes, the reuse retry, and the
restores after a no-change group and after the config-only push.
@github-actions

Copy link
Copy Markdown

Review

  • No new findings in the current diff.
  • All previously raised findings are resolved.
  • The PR description documents sufficient automated testing and a representative real-CLI end-to-end verification.

@jeff-r2026
jeff-r2026 merged commit f836db4 into Tencent:main Sep 29, 2026
13 checks passed
@SaulMoro
SaulMoro deleted the fix/881-push-dirty-env branch September 29, 2026 11:49
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.

[bug] push refuses the env files env add leaves in a standalone clone

2 participants