Repository navigation
scaffold-project.py: a push whose stored login the host refuses (Authentication failed), or whose ssh key it refuses (Permission denied (publickey)), names how to replace it and the --reuse-empty-repo re-run before the ruleset hint, in any locale (#993) - #1012
Conversation
…hentication failed`), or whose ssh key it refuses (`Permission denied (publickey)`), names how to replace it and the `--reuse-empty-repo` re-run before the ruleset hint, in any locale (#993) A first scaffold's push, or a re-run's, that git made with a login its credential helper holds and the host refused printed only the ruleset hint, whose one exit is a re-run refused the same way. So did a push an `insteadOf` or `pushInsteadOf` rule sends over ssh where the host took no key. Each now names its fix, then the re-run, then the hint, as a push git has no login for has since #971. Each push runs under `LC_ALL=C` (`PUSH_ENV`), because git translates `Authentication failed for` (measured under a catalogue). Any other refusal, a 403 included, prints what it printed before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR enhances scaffold push failure diagnostics by detecting refused HTTPS credentials and SSH keys, providing case-specific replacement and rerun guidance before the ruleset hint, and forcing pushes to use English Git output so detection works across locales while leaving unrelated failures unchanged. Sequence diagram for locale-safe scaffold push handlingsequenceDiagram
participant Scaffold
participant Git
participant Host
Scaffold->>Git: push(..., env=PUSH_ENV)
Note over Git: LC_ALL=C and terminal prompts disabled
Git->>Host: Authenticate and push
Host-->>Git: Accept or refuse login/key
Git-->>Scaffold: Output and exit status
Scaffold->>Scaffold: _login_remedy(output, repo, reused)
alt Login refused
Scaffold-->>Scaffold: Print credential replacement and rerun guidance
else SSH key refused
Scaffold-->>Scaffold: Print SSH_KEY_CHECK and rerun guidance
else Other refusal
Scaffold-->>Scaffold: Print no remedy
end
Scaffold-->>Scaffold: Print RULESET_HINT
State diagram for scaffold rerun after refused credentialsstateDiagram-v2
[*] --> InitialPush
InitialPush --> LoginRefused: LOGIN_REFUSED
InitialPush --> KeyRefused: KEY_REFUSED
InitialPush --> NoLogin: NO_LOGIN
InitialPush --> OtherFailure: Other output
LoginRefused --> CredentialReplacement: replace rejected login
KeyRefused --> SSHKeyCheck: provide accepted SSH key
NoLogin --> LoginSetup: configure Git login
CredentialReplacement --> Rerun
SSHKeyCheck --> Rerun
LoginSetup --> Rerun
Rerun --> Completed: host accepts credentials
Rerun --> LoginRefused: credential still rejected
Rerun --> NoLogin: credential unavailable
OtherFailure --> [*]
Completed --> [*]
Flow diagram for scaffold push refusal diagnosticsflowchart TD
A[Git push with PUSH_ENV] --> B{Refusal output matches}
B -->|NO_LOGIN| C[Explain missing login and credential options]
B -->|LOGIN_REFUSED| D[Explain rejected credential and replacement options]
B -->|KEY_REFUSED| E[Explain rejected SSH key and run SSH_KEY_CHECK]
B -->|Other refusal| F[Keep existing failure output]
C --> G[Print rerun guidance]
D --> G
E --> G
F --> H[Print RULESET_HINT]
G --> H
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
(lane openRepoShape-3, 2026-10-19T22:01:43Z) RV1012, lane 3's independent review of #1012 at head d87263e: READY. No MUST, no SHOULD, four NITs. GitHub's Detection, measured on real git 2.43 against a local server that answers 401, with
PUSH_ENV:
Remedy text:
Tests (each file alone, stub gh exit 97, GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, EMAIL set):
Mutants (each against all of test_scaffold_reuse_seeded_legs.py):
Merges (git merge-tree --write-tree), all clean:
NITs:
Skipped to save usage: a real sshd, a Windows run, test_dry_run_asks_remotes.py, the full suite. Lane: openreposhape-3 (openRepoShape-3) |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 20 hours by commenting |



Closes #993.
What changed
The issue's case: a stored login the host refuses (
fatal: Authentication failed for '<url>')._refused_pushpasses git's output to_login_remedy, which now picks the sentence for the case the output shows:NO_LOGIN: git could not get a login. Its words are unchanged, byte for byte.LOGIN_REFUSED = ("Authentication failed for ",): a case of its own, as the Direction asked, not a phrase added toNO_LOGIN. The remedy says the host refused the login git sent, so it is wrong, expired or revoked, and a re-run is refused too until git has one the host takes. To replace it:gh auth setup-gitmakes git use the loginghhas, or a new token goes in the credential helper, or the ssh clause (SSH_KEY_CLAUSE) applies.The comment's case: a push an
insteadOfrule sends over ssh, refused for its key (Permission denied (publickey)).KEY_REFUSED = ("Permission denied (publickey",)names thessh -Tcheck against the user, host and port the rule names (SSH_KEY_CHECK, split out ofSSH_KEY_CLAUSEwith no byte of that clause changed), then the re-run, then the hint.gh auth setup-gitas the fix, since that changes nothing over ssh.Any other refusal, a ruleset's above all, prints exactly what it printed before.
Decisions the Direction left open
Translation was measured, so each push now runs under
LC_ALL=C.Authentication failed forto be measured before the detection relied on it. git 2.43, given a catalogue that translates the phrase, printsXX-FATAL: XX-Anmeldung abgelehnt fuer 'http://127.0.0.1:34051/acme/Atlas-spec.git/'; without one it printsfatal: Authentication failed for '...'. So git translates it.PUSH_ENV = {**REMOTE_ENV, "LC_ALL": "C"}, the Direction's first option. Reading the HTTP status would needGIT_TRACE_CURLand would put headers on stderr.NO_LOGINcomment that said pushes do not takeLC_ALL=Cis rewritten to match.The ssh match reads
Permission denied (publickeywithout the closing parenthesis. A host that offers more ways to log in lists them afterpublickey, as in(publickey,password). These are OpenSSH's own words, and OpenSSH does not translate them.The ssh remedy names
url.<base>.pushInsteadOfas well asinsteadOf. The common setup that fetches over https and pushes over ssh sends only the push over ssh.I checked the
gh auth setup-gitclaim before the words relied on it. gh writes an emptyhelper =before its own (setup-project.py's_credential_helper_configuredsays so). On git's side, I probedgit credential fillwith three helpers in this order:storehelper;storehelper.The helper standing in for gh's answered. The stale helper before the empty line was not asked, and the one after it was not asked once gh's stand-in had answered. I did not run the real gh, which this lane uses only for reads and its named writes.
The re-run sentence holds after a refused login. On a 401, git asks its helpers to forget the refused login: the
storefile's line was gone after the probe. A re-run before the login is replaced is then refused withNO_LOGIN's words, and has the same exit. So the sentence says "a re-run is refused too until git has one the host takes", not "the same way".No docs change. No refusal code was added, and no doc quotes the push remedies (scaffold-project.py: a first scaffold's pushes and a
--reuse-empty-repore-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) #971 changed none either). The README's login step ("without it the three repositories are created and the push fails") still holds.Out of scope, and why
a-403.family.py init's andadopt-project.py's pushes (FAMILY_RULESET_HINT,ADOPT_RULESET_HINT): the Direction says these are their own issues.No supported authentication methods available: not read, and not probed._take_held_seed's fetch stays underREMOTE_ENV. Its output names no remedy.Tests
Each test file was run alone under a stub
gh(exit 97) first on PATH, withGH_TOKEN=bogus, an emptyGH_CONFIG_DIRandEMAIL=tests@openreposhape.invalid, astimeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed <file>:tests/test_scaffold_reuse_seeded_legs.py: 64 passed in 50.32stests/test_scaffold_e2e.py: 46 passed in 18.94stests/test_scaffold_pin_and_reuse.py: 75 passed in 27.24stests/test_dry_run_asks_remotes.py: 32 passed in 10.48stests/test_ruleset_hint_tracking_branch.py(pinsRULESET_HINT): 23 passed in 32.50stests/test_adopt_late_failure.py(pinsRULESET_HINT): 88 passed in 35.83stests/test_git_output_decoding.py: 67 passed in 2.50stests/test_agents_md_codes.py: 47 passed in 1.91stests/test_cli_reference.py: 23 passed in 29.88stests/test_update_add_codes_documented.py: 6 passed in 0.43stests/test_repo_hygiene.py: 100 passed in 2.41stests/test_no_auto_maintenance.py: 60 passed in 11.77sNew and changed tests, all in
tests/test_scaffold_reuse_seeded_legs.py:test_a_push_whose_stored_login_the_host_refuses_names_the_fixcoversa-first-runanda-re-run, eachas-it-isandtranslated.--reuse-empty-repore-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) #971's 401 server, withstoreholdingsomeone:badtokenfor it.REFUSED pushing the spec legand the hint.XX_GITgains a translation ofAuthentication failed for '%s'.test_a_push_ssh_was_refused_the_key_for_names_the_fixcoversinsteadOfandpushInsteadOf, eachas-it-isandtranslated. It runs a first run pushing over the stand-in ssh, which printsPermission denied (publickey).and exits 255.test_what_a_refused_push_printsgains these rows:login-refused,login-refused-on-the-re-run,ssh-refused-the-key-on-the-re-run,ssh-refused-the-key-of-more-waysanda-403.ssh-refused-the-keyrow now expects the ssh remedy. At main it pinned the hint alone, which was this issue's gap.test_every_git_command_that_reaches_a_remote_runs_under_the_rulenow expects the three pushes underPUSH_ENVand pins its value.Mutants
Each mutant was run against the new and changed tests (
-kover the four tests above and the two existing pty push tests), and the file was restored after each:LOGIN_REFUSED = (), so the detection is narrowed toNO_LOGINlogin-refused,login-refused-on-the-re-run"Authentication failed for "put intoNO_LOGINKEY_REFUSED = ()ssh-refused-the-key,-on-the-re-run,-of-more-waysPUSH_ENV = REMOTE_ENV, so pushes are translatedtranslated-a-first-run,translated-a-re-run, the env rule testREMOTE_ENVLOGIN_REFUSED = ("failed",)a-rulesetKEY_REFUSED = ("denied",)a-403KEY_REFUSEDnarrowed to"Permission denied (publickey)."ssh-refused-the-key-of-more-waysa-ruleset,a-403SSH_KEY_CLAUSE(which names setup-git)Merges
git merge-tree --write-treeof the headd87263e4ca81066c908930863dfc909607dc9ebc:3864b18cc99a35a0e11344f591fae7f76cbfbe72: clean. Main moved since the base04923f10d, but not in either file this changes.l3-989-999-scaffold-names-folder-help37b0fc438d2516e921a4c5dbbfb6dc8bf629b252: clean. It changesscaffold-project.pyin other functions.wip/l3-989-usage-20261019099eca4ae12b427db7eba5f9225ac1dd11a3f621: clean.gh pr list --state openshows no other open PR touchingscaffold-project.pyortests/test_scaffold_reuse_seeded_legs.py.Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Improve scaffold push failure messages so users can replace rejected credentials or SSH keys and safely re-run the scaffold.
New Features:
Bug Fixes:
Enhancements:
Tests: