Skip to content

Layer GIT_SSH_COMMAND on the inherited env when pushing a branch - #223

Merged
blooop merged 1 commit into
mainfrom
wayfinder/devlaunch-212
Aug 15, 2026
Merged

blooop merged 1 commit into
mainfrom
wayfinder/devlaunch-212

Conversation

@blooop

@blooop blooop commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

Closes #212

BranchManager.push_branch_to_remote built env = {"GIT_SSH_COMMAND": ssh_command} — replacing the inherited environment rather than extending it. So the one git call that was told which key to authenticate with ran with no PATH to find ssh on, no HOME to read ~/.ssh/known_hosts or the key's own config from, and no SSH_AUTH_SOCK to reach the agent. Naming a key was what broke the auth it was meant to set up, and only on the machines that pass one. Same env-replacement hazard #187 closed at the locale pin; same {**os.environ, ...} shape.

Pinned at the argv/env seam the ticket named, mirroring the SSH_AUTH_SOCK assertion test_already_exists_answer_survives_a_translated_git already makes: with a sentinel agent socket in the environment, the env handed to the subprocess must carry GIT_SSH_COMMAND and still carry the inherited SSH_AUTH_SOCK and PATH. Red before green — the new test failed with KeyError: 'SSH_AUTH_SOCK' against the old line.

Also in scope, from #211's Standards review: the two places that classify git's outcome by reading stderr text disagreed on the None-guard — the fetch path used e.stderr or "", branch creation read e.stderr unguarded. A CalledProcessError whose stderr was never captured therefore raised TypeError: argument of type 'NoneType' is not a container or iterable — naming neither the branch nor the cause — where every other failure raises Failed to create branch. Its own red test drove that, and the two sites now read the same.

pixi run ci green locally: 1748 passed, pylint 10.00/10. CHANGELOG entry under ## [Unreleased] → ### Fixed, beside #187's.

🤖 Generated with Claude Code

Summary by Sourcery

Layer ssh key configuration onto the inherited environment when pushing branches and harden branch creation error handling for stderr-less failures.

Bug Fixes:

  • Ensure git pushes with a named ssh key preserve PATH, HOME, SSH_AUTH_SOCK and the rest of the inherited environment instead of replacing it.
  • Guard branch creation stderr inspection so failures without captured stderr still raise a clear "Failed to create branch" error rather than a TypeError.

Documentation:

  • Update the changelog under Unreleased/Fixed to document the environment preservation during ssh key pushes and the improved branch creation error handling.

Tests:

  • Add tests asserting ssh key pushes keep the inherited environment variables while setting GIT_SSH_COMMAND.
  • Add a test verifying local branch creation handles CalledProcessError with stderr=None by raising a RuntimeError.

push_branch_to_remote built env = {"GIT_SSH_COMMAND": ssh_command}, which
replaces the inherited environment rather than extending it. The one git
call that was told which key to authenticate with therefore ran with no
PATH to find ssh on, no HOME for known_hosts, and no SSH_AUTH_SOCK to
reach the agent -- naming a key was what broke the auth it set up. Same
env-replacement hazard #187 closed at the locale pin, same {**os.environ,
...} shape.

Pinned at the argv/env seam, mirroring the SSH_AUTH_SOCK assertion the
locale test already makes: the sentinel inherited variable must survive
alongside GIT_SSH_COMMAND.

Also aligns the two textual-stderr classifiers, as #211's review asked.
The fetch path guarded `e.stderr or ""`; branch creation read it
unguarded, so a failure git wrote nothing to stderr for raised a
TypeError naming neither the branch nor the cause instead of the
"failed to create branch" error every other failure raises.

Closes #212

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes environment handling when pushing branches with a specific SSH key and aligns stderr handling for git branch creation failures, adding tests and changelog entries for both behaviors.

Sequence diagram for pushing a branch with a named ssh key

sequenceDiagram
    participant BranchManager
    participant Subprocess as subprocess
    participant Git as git
    participant SSH as ssh

    BranchManager->>BranchManager: push_branch_to_remote(base_repo_path, branch, remote, ssh_key_path)
    alt ssh_key_path is provided
        BranchManager->>BranchManager: ssh_command = "ssh -i ssh_key_path -o IdentitiesOnly=yes"
        BranchManager->>BranchManager: env = {**os.environ, GIT_SSH_COMMAND: ssh_command}
    else no ssh_key_path
        BranchManager->>BranchManager: env = None
    end
    BranchManager->>Subprocess: subprocess.run(["git", "push", remote, branch], cwd=base_repo_path, env=env, check=True)
    Subprocess->>Git: execute git push with env
    Git->>SSH: run ssh using GIT_SSH_COMMAND and inherited PATH, HOME, SSH_AUTH_SOCK
    SSH-->>Git: authenticate using specified key and agent
    Git-->>Subprocess: push result
    Subprocess-->>BranchManager: CompletedProcess or CalledProcessError
Loading

File-Level Changes

Change Details Files
Ensure GIT_SSH_COMMAND for SSH key pushes is layered on top of the inherited environment rather than replacing it.
  • Change push_branch_to_remote to build env by copying os.environ and overriding GIT_SSH_COMMAND when an ssh_key_path is provided
  • Add tests that assert GIT_SSH_COMMAND is set while SSH_AUTH_SOCK and PATH from the inherited environment are preserved
devlaunch/worktree/branch_manager.py
test/test_worktree_branch_manager.py
Guard stderr access in create_local_branch so branch creation failures without captured stderr are handled consistently.
  • Normalize CalledProcessError.stderr to an empty string before substring checks for "already exists"
  • Use the normalized stderr when logging and raising RuntimeError for non-benign failures
  • Add a test that a stderr-less CalledProcessError results in a RuntimeError("Failed to create branch")
devlaunch/worktree/branch_manager.py
test/test_worktree_branch_manager.py
Document the environment layering and stderr handling fixes in the changelog.
  • Add a Fixed entry under Unreleased describing the SSH environment layering change
  • Describe the aligned stderr None-handling for git outcome classification
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#212 Ensure BranchManager.push_branch_to_remote layers GIT_SSH_COMMAND onto the inherited environment instead of replacing it, and add a test asserting that the inherited environment (e.g., SSH_AUTH_SOCK, PATH) survives. ✅
#212 Align stderr handling for branch creation with the existing fetch path by guarding e.stderr (using e.stderr or "") so that failures with None stderr are classified correctly and raise a meaningful RuntimeError, and add a test for this behaviour. ✅

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.96%. Comparing base (67e2acf) to head (1f8d2b9).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #223   +/-   ##
=======================================
  Coverage   92.96%   92.96%           
=======================================
  Files          24       24           
  Lines        3679     3680    +1     
=======================================
+ Hits         3420     3421    +1     
  Misses        259      259           
Files with missing lines Coverage Δ
devlaunch/worktree/branch_manager.py 100.00% <100.00%> (ø)

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This was generated by AI during review.

Two-axis review of #223 against ticket #212 at head 1f8d2b9. Preflight: all checks green, non-empty diff (3 files), MERGEABLE.

Standards

Verified clean: pixi run ci exit 0 from a fresh clone (pylint 10.00/10, 1748 passed, branch_manager.py at 100% coverage); CHANGELOG under ## [Unreleased] → Fixed; no hardcoded home paths; both tests match the file's sentence-naming and mock_run.call_args[1].get("env") idiom; no docs owed (no CLI surface changed).

  • Non-blocking — Duplicated Code: test_push_branch_to_remote_with_ssh_key is now a strict subset of the new env test; it can only fail when the new one already has. (The builder declared keeping it deliberately — "deleting a passing test is not part of this slice" — which is sound; folding is a rider.)
  • Non-blocking — Shotgun Surgery (latent): env layering now hand-repeated at branch_manager.py:87/:190 + repo_manager.py:402, and the None-guard at branch_manager.py:94 + repo_manager.py:406, with dl.py:3083 stating the hazard a third way in prose. Two sites is thin for a _git_env() helper; a third would earn it.
  • Non-blocking — push_branch_to_remote still formats e.stderr raw at :203, so a stderr-less push failure reads "Failed to push branch to remote: None" — the same complaint class this PR's changelog makes, one arm over.
  • Non-blocking — pre-existing, adjacent: :185 interpolates ssh_key_path into the shell string unquoted; a path with a space breaks the command the fix protects. Declared out of scope by the builder with sound reasoning; worth its own ticket.

Spec

All ticket items verified with mutations reproduced. Nothing blocks.

  • "{**os.environ, ...} … is the shape that works" — :190 is exactly that; reverting the line fails the new test with KeyError: 'SSH_AUTH_SOCK', the ticket's stated failure. Restored.
  • "a test at the argv/env seam … mirror test_already_exists_answer_survives_a_translated_git" — same seam, same sentinel shape, adds PATH; right function (push, not creation).
  • "worth aligning in the same pass" (None-guard) — both sites now e.stderr or ""; mutating branch_manager's back is caught (TypeError, 1 failed).
  • Non-blocking — the other half of the aligned pair (repo_manager.py:406) mutates back to unguarded with the suite still green: pre-existing, load-bearing, unpinned. Rider for a future toucher.
  • Non-blocking — ticket names PATH/HOME/SSH_AUTH_SOCK; the test pins two of three (the splat covers all).
  • Nothing undeclared rode along; full suite 1748 passed / 24 deselected.

Verdict

Approve — no blocking findings on either axis. Riders for future touchers: fold the subsumed ssh-key test, pin repo_manager.py:406's guard, consider _git_env() at a third site, quote ssh_key_path (own ticket). GitHub refuses --approve from the PR author's account, so this is posted as a comment; this written verdict governs. Merging with a merge commit per repo rules.

@blooop
blooop merged commit fd4c50f into main Aug 15, 2026
12 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-212 branch August 15, 2026 08:24
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.

push_branch_to_remote replaces the whole environment with {"GIT_SSH_COMMAND": ...}

1 participant