Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,13 @@ manifest, sets the topic `xf-project-atlas` on all three, clones the root as
> refused is finished by an operator holding the bypass right re-running it.
> A leg with other commits is refused, and a root that HAS commits is a live
> repository, which `adopt-project.py` converts in place instead.
> That re-run, as every run, turns off git's own terminal prompt and no
> other: an ssh passphrase or host-key prompt, a credential helper that
> prompts, or `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass` can still make it
> wait. A key in an ssh agent, a helper that answers without prompting, or
> `GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with anything your
> `core.sshCommand` adds) set in your shell for the run, which makes ssh fail
> at once instead, keeps it from waiting.
> `--visibility internal` is the third value every tool here accepts; only a
> GitHub Enterprise Cloud organisation offers it at all, and one that does not
> is refused by GitHub rather than by this tool.
Expand Down
11 changes: 10 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,16 @@ options:
right finishes a run a ruleset refused by re-running
it with this. A root with commits, and a leg with any
other, is still refused: that is a live repository,
not a slot.
not a slot. A run with it on GitHub, like every run
there, turns off git's own terminal prompt and no
other, so it can still wait at an ssh passphrase or
host-key prompt, at a credential helper that prompts,
or at `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass`:
a key loaded into an ssh agent, a helper that answers
without prompting, or `GIT_SSH_COMMAND='ssh -o
BatchMode=yes'` (with anything `core.sshCommand` adds)
set for the run, which makes ssh fail at once instead,
keeps it from waiting.
--pin openProduct@COMMIT
declare a neutral-product pin, e.g. --pin
openGlass@<40 hex>. It writes
Expand Down
22 changes: 15 additions & 7 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@

<!-- reads README.md § preamble: blob ddadd23a162129f7e64fefce899105661440c05c -->

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -612,7 +612,7 @@ <h4>The one thing to read before electing it</h4>

<section id="start">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<p class="eyebrow">Setup</p>
<h2>Start here</h2>
Expand Down Expand Up @@ -1092,7 +1092,7 @@ <h4>What you have afterwards &mdash; the same on every platform</h4>

<section id="partway">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<p class="eyebrow">Setup</p>
<h2>If it fails part-way</h2>
Expand Down Expand Up @@ -1124,6 +1124,14 @@ <h2>If it fails part-way</h2>
re-running it. A leg with other commits is refused, and a root that HAS commits is a live
repository, which <a href="#adopt"><code>adopt-project.py</code></a> converts in place
instead.</li>
<li>The <code>--reuse-empty-repo</code> re-run, as every run, turns off git&rsquo;s own
terminal prompt and no other: an ssh passphrase or host-key prompt, a credential helper
that prompts, or
<code>GIT_ASKPASS</code>, <code>SSH_ASKPASS</code> or <code>core.askPass</code> can still
make it wait. A key in an ssh agent, a helper that answers without prompting, or
<code>GIT_SSH_COMMAND='ssh -o BatchMode=yes'</code> (with anything your
<code>core.sshCommand</code> adds) set in your shell for the run, which makes ssh fail at
once instead, keeps it from waiting.</li>
<li><code>--visibility internal</code> is the third value every tool here accepts; only a
GitHub Enterprise Cloud organisation offers it at all, and one that does not is refused by
GitHub rather than by this tool.</li>
Expand All @@ -1133,7 +1141,7 @@ <h2>If it fails part-way</h2>

<section id="ci">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -1196,7 +1204,7 @@ <h2>Reading private legs in CI</h2>

<section id="legs">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<!-- reads README.md § The three legs: blob 4ec313e8424665e41fc92a4147808e0bd4901d78 -->

Expand Down Expand Up @@ -1452,7 +1460,7 @@ <h3>The InkRouter example</h3>

<section id="doublepin">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<!-- reads README.md § The double pin, and the lockstep invariant: blob 57fffa08ffad5476f8eae73dd3e8494a02e050c9 -->

Expand Down Expand Up @@ -1751,7 +1759,7 @@ <h2>Is this repository compliant? <code>openRepoShape --doctor</code></h2>

<section id="park">

<!-- reads README.md § Starting a project in a new organisation: blob 8f25ef0d24d075f9e7c2f4ed3279079ebe061105 -->
<!-- reads README.md § Starting a project in a new organisation: blob ffaedeefe61720051098400cbf2212de50b923f3 -->

<!-- reads README.md § Carrying in-flight work to another workstation: blob b84512ad39c09085dfe6bf7ea694017f5e28c1dd -->

Expand Down
86 changes: 73 additions & 13 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,20 @@ def main(argv: list[str] | None = None) -> int:
"right finishes a run a ruleset refused by "
"re-running it with this. A root with commits, "
"and a leg with any other, is still refused: "
"that is a live repository, not a slot.")
"that is a live repository, not a slot. A run "
"with it on GitHub, like every run there, turns "
"off git's own terminal prompt and no other, so "
"it can still wait at an ssh passphrase or "
"host-key prompt, at a credential helper that "
"prompts, or at "
"`GIT_ASKPASS`, `SSH_ASKPASS` or "
"`core.askPass`: a key loaded into an ssh "
"agent, a helper that answers without "
"prompting, or "
"`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with "
"anything `core.sshCommand` adds) set for the "
"run, which makes ssh fail at once instead, "
"keeps it from waiting.")
parser.add_argument("--pin", action="append", default=[],
metavar="openProduct@COMMIT",
help="declare a neutral-product pin, e.g. "
Expand Down Expand Up @@ -503,11 +516,22 @@ def seed_tree(role: str, values: dict, tracking: str, scratch: Path) -> str:
#: read, and a push, a first run's as a re-run's, is refused with how to give
#: git a login (`_refused_push`).
#:
#: ssh, a credential helper and an askpass program are not turned off: they
#: are how people give git a credential, and each of them still asks under
#: this rule (#915's probes). Whether the rule must never wait for input,
#: ssh run with `BatchMode=yes` for one, is #915's open ruling question; it
#: is decided here, once, for every command above.
#: THE RULE TURNS OFF GIT'S OWN PROMPT AND NO OTHER (#915, U037). ssh, a
#: credential helper and an askpass program (`GIT_ASKPASS`, `SSH_ASKPASS`,
#: `core.askPass`) are the operator's, and are left as they are: they are
#: how people give git a credential, and turning one off would refuse a read
#: or a push that works. So every command above, a first run's push as a
#: re-run's probe and push, can still wait for input: at ssh's passphrase or
#: host-key prompt, at a credential helper that prompts, or at an askpass
#: program, each measured waiting until killed. Nor is ssh given
#: `BatchMode=yes` here: git's `GIT_SSH_COMMAND` overrides a configured
#: `core.sshCommand`, so setting it would replace the operator's own ssh
#: command. Brett Heap ruled it on 2026-10-21, "U039 and U037: take your
#: recommendations": say it where it is true. The `--reuse-empty-repo` help,
#: `docs/cli.md`, the README, the handbook and each remedy that sends a
#: person into that re-run on GitHub say a prompt can still come, and how a
#: person keeps a run from waiting in their own setup (`PROMPTS_STILL_ASK`).
#: `RULESET_HINT`, the ruleset's refusal, does not say it yet (#1108).
REMOTE_ENV = {"GIT_TERMINAL_PROMPT": "0"}

#: What `only_commit`'s two commands run with: the prompt rule, and git's
Expand All @@ -529,6 +553,28 @@ def seed_tree(role: str, values: dict, tracking: str, scratch: Path) -> str:
#: a push that goes through shows nothing of git's.
PUSH_ENV = {**REMOTE_ENV, "LC_ALL": "C"}

#: What each remedy that sends a person into a `--reuse-empty-repo` re-run
#: says of the prompt rule, after "That re-run" (#915, U037): the prompts
#: `REMOTE_ENV` leaves as the operator set them up, and the three ways to keep
#: a run from waiting at one, each in the person's own setup. ssh under
#: `BatchMode=yes` refuses the host-key and passphrase prompts and fails at
#: once (measured: the probe is refused at once, as `scaffold-remote-exists`
#: with ssh's `Host key verification failed.`). `GIT_SSH_COMMAND` replaces
#: `core.sshCommand`, so the person carries over what theirs adds. Said by
#: `_exists_remedy` and `_login_remedy`; not under `--local-remote-dir`,
#: whose paths on disk git reads with no ssh, helper or askpass at all.
#: `RULESET_HINT`, the ruleset's refusal, also sends a person into that
#: re-run and does not say it yet (#1108).
PROMPTS_STILL_ASK = (
"turns off git's own terminal prompt and no other, so it can still wait "
"at an ssh passphrase or host-key prompt, at a credential helper that "
"prompts, or at `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass`: to keep "
"it from waiting, load your key into an ssh agent, use a helper that "
"answers without prompting, or set "
"`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with anything your "
"`core.sshCommand` adds) in your shell for the run, which makes ssh fail "
"at once instead")


def only_commit(url: str,
scratch: Path) -> tuple[str, str, tuple[str, ...]] | None:
Expand Down Expand Up @@ -756,7 +802,7 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,
+ ("" if local else " on GitHub")
+ ("" if empty else " and has commits"),
_exists_remedy(role, empty, reuse_flag, seed is not None,
unread))
unread, local))
return reused


Expand All @@ -783,7 +829,8 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,


def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
seeded: bool = False, unread: str | None = None) -> str:
seeded: bool = False, unread: str | None = None,
local: bool = False) -> str:
"""The remedy line for a remote that is already there.

Each situation answers to its own exit, and a refusal that named only
Expand All @@ -798,7 +845,15 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
git logs in with an ssh key, and the command named is still the https
URL this run gave git. So the ssh key's clause (`SSH_KEY_CLAUSE`) stands
beside the `setup-git` one, each saying where it applies.

EACH EXIT THAT IS THE `--reuse-empty-repo` RE-RUN SAYS WHICH PROMPTS IT
CAN STILL WAIT AT (#915, U037): its probe and its pushes turn off git's
own prompt alone, and leave ssh, a credential helper and an askpass
program as the operator set them up (`PROMPTS_STILL_ASK`). Not where
`local` (`--local-remote-dir`): there git reads paths on disk, and none
of them is asked.
"""
still = "" if local else " That re-run " + PROMPTS_STILL_ASK + "."
if role != "assembly":
if unread:
return (f"Remediation: git could not read it ({unread}), so "
Expand All @@ -808,7 +863,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
+ SSH_KEY_CLAUSE + "), then re-run: with "
"--reuse-empty-repo a leg whose one commit is that seed "
"is reused, and a leg holding anything else is still "
"refused. There is no --force.")
"refused. There is no --force." + still)
if empty or seeded:
return ("Remediation: "
+ ("it has ZERO commits" if empty else
Expand All @@ -817,7 +872,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
+ f", so re-run with --reuse-empty-repo to use it as the "
f"{role} leg. A run an organisation's ruleset refused is "
"finished that way, by an operator holding the bypass "
"right.")
"right." + still)
return ("Remediation: it holds commits other than the one seed this "
"scaffold would push to it, so it is not a slot this scaffold "
"may fill. Delete it, or scaffold under a different "
Expand All @@ -828,7 +883,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
if empty and not reuse_flag:
return ("Remediation: it has ZERO commits, so it is a reserved name "
"rather than a live project — re-run with --reuse-empty-repo "
"to use it as the assembly root.")
"to use it as the assembly root." + still)
return ("Remediation: it HAS commits, so it is a live repository and this "
"is not a scaffold. Use `adopt-project.py plan --source "
"<org>/<repo> --project <Project>` to convert it in place, which "
Expand Down Expand Up @@ -2090,7 +2145,11 @@ def _login_remedy(output: str, repo: str, reused: bool) -> str:
(`setup-project.py`'s `_credential_helper_configured` reads it so). So
it replaces a refused login too. Over ssh it changes nothing, so a key
ssh was refused is told the ssh check alone (`SSH_KEY_CHECK`). A run
that was itself the re-run (`reused`) is re-run as it was."""
that was itself the re-run (`reused`) is re-run as it was. Last, that
re-run's prompts: git's own is off and ssh's, a helper's and an askpass
program's are not (`PROMPTS_STILL_ASK`, #915 U037). A login git could not
get was refused rather than asked for, and the login a person gives git
next, an ssh key above all, can ask on the re-run."""
if any(words in output for words in NO_LOGIN):
fix = (f"git could not get a login for {repo}: every push this "
"scaffold makes runs with git's terminal prompt off, so where "
Expand Down Expand Up @@ -2121,7 +2180,8 @@ def _login_remedy(output: str, repo: str, reused: bool) -> str:
"through setup.sh) and a new --work-dir or none")
return (f"\nRemediation: {fix}, then re-run {again}: it reuses the "
"repositories this run left empty, and a leg whose one commit is "
"the seed this scaffold pushes.")
"the seed this scaffold pushes. That re-run "
+ PROMPTS_STILL_ASK + ".")


def _refused_push(failed: CommandFailed, what: str, repo: str,
Expand Down
11 changes: 11 additions & 0 deletions tests/readme-cap.d/915.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
+7
2026-10-21
Seven lines for the prompts a `--reuse-empty-repo` re-run can still wait at
(#915, U037; Brett Heap, 2026-10-21, "U039 and U037: take your
recommendations"). The scaffold turns off git's own terminal prompt alone and
leaves ssh, credential helpers and askpass programs as the operator set them
up, so the re-run this section sends a person into can stop at an ssh
passphrase or host-key prompt, a prompting helper or `GIT_ASKPASS`,
`SSH_ASKPASS` or `core.askPass`, each measured waiting until killed. The lines
name those prompts and the three ways a person keeps the run from waiting,
`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` among them, and nothing else.
Loading
Loading