fix(push): publish the env files env add leaves in a standalone clone (#881) - #885
Merged
Merged
Conversation
…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.
|
Findings
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.
|
Findings
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.
|
Findings
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.
|
Review
|
…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.
|
Review
|
6 of 9 tasks
jeff-r2026
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
env addthenpushworks again in a standalone clone: push keeps the env filesenv addleft instead of refusing them, and publishes only the ones you select.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 deletedenv.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
Test Plan
npx tsc --noEmitpassesnpm run lintpassesnpx vitest runpasses (5263 passed, 1 skipped; run withCLAUDE_CONFIG_DIRunset, see fix(tests): prevent model tests from overwriting Claude config (P1) #890)push-env.test.ts: realenvAdd()+push()on a bare remote and clone)npm run test:e2e: 368 passed, 26 skipped.mainenv.yamleditPaths: README.mdonly)env/env.yamltoofinally)env.yamlstill refuses; index and edit survive--branchnames an existing local branch) → edit and its0600mode surviveenv.yaml, deselectteam/env.yaml→ only the selected file is pushed; the other stays dirtygit pushrejected after the commit → clone back onmainwith the edit dirty; retry publishes itenv/ops/env.yamlfromenv add --role ops→ restored, still0600, after the rollback'sclean -fdand after a rejected pushenv.yamlhand edit → same bytes after the rollbacknochange) → edit survives--branchwith only reuse groups, config-only push with no change → edit survivesteamai.yamledit in the config group,git pushrejected after the commit → kept onmainPull failed; state savedenv.yamlstill refusesEach 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.yamland restore-failure rows, f465062 for the mode, Latin-1 and reuse-retry rows). Thenochangeand config-only rows pass on f465062 too; they pin restores that were untested, and each fails when its restore is removed. The--rolerow passes on bf5098b too: thewriteFilehelper already recreates the directory, and the tests pin that (they fail withENOENTunder a barefs.writeFile). The restore-failure row injects the write error through a mockedwriteFile; it has no real-CLI record.Real CLI, sandbox HOME,
CLAUDE_CONFIG_DIRunset, generic-git fixture:Review fixes, same fixture, commit before the fix vs this head:
Round 3, same fixture, bf5098b vs this head:
Adversarial round, same fixture,
umask 022, f465062 vs this head:(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
pushCoreand one sweeper entry inpushGroup; 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 filespushItemcopied for the selection, so staging them by path gives the same commit (single-repo tests pass unchanged).pullRepoloses the edit when the pull fails afterreset --hard. Thefinallykeeps it; a test covers it.pendingTeamConfigre-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.pushRepoBranchtreats such a diff as no change for every resource.teamai.yamlkeeps its text round-trip and does not keep a hand-set0600acrossreset --hard(already onmain, fix(push): honor explicit branches and protect dirty team clones #690); left for the separateteamai.yamlfollow-up.teamai.yaml) are back on the default branch, so the next run retries them under a new branch.main:pushTeamConfigOnly(ateamai.yaml-only push) whosegit pushfails 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.teamai.yamltoday: a teammate's upstream change to the sameenv.yamlbetweenenv addandpushis overwritten on the pushed branch.env addpulls first, so the window is small.main: a modifiedenv.yamlis listed as "(new)".