Skip to content

bootstrap.py, the assembly root's template and the family holder's, ends in a FileNotFoundError traceback, exit 1, when git is not on PATH, because the root's step (a) calls recorded_gitlink, its offline upstream notice calls _try_git and the holder's step (a) runs git submodule update, and nothing catches OSError, where the docstrings say step (a) "never raises" and the notice "is silent when it cannot answer", so the first command a new engineer runs after cloning answers a missing git with a stack trace and the code its own bootstrap FAILED line uses: the third sibling of #368 and #412 #463

Closed as not planned

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by this lane's review of PR #446 (#412, validate-pins.py with no git), from the writer's own list of what that PR leaves out: "scripts/bootstrap.py with no git gives FileNotFoundError: [Errno 2] ... 'git', exit 1". The review rated it a follow-up of its own, lower priority: "bootstrap's job is git, so it only needs a clean refusal in place of a traceback". It is the sibling of #368 (validate-family.py) and #412 (validate-pins.py): the same missing git, a third script. Re-verified on main dd8d0ea (templates/assembly-root/scripts/bootstrap.py sha256 ae1426fd…, templates/family-root/scripts/bootstrap.py 0a0fb055…, scripts/repo_shape.py ef0251ef…), with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh that exits 97 (never called). Neither PR #446 nor PR #406 touches either file; the one open PR that does is #456 (#391, main's print of the manifest's name). The exposure is small: the engineer usually cloned the repository, so git was there a minute ago, and what removes it is a shell with a stripped PATH or a container image that dropped it. What this is about is the sentence and the code: bootstrap is "the one command an engineer runs after git clone --recurse-submodules", and its exit 1 reads as "a validator failed".

What is wrong. Three places run git and nothing around them catches OSError:

U=<openRepoShape checkout>   # a clean checkout at dd8d0ea
# Runs in bash on Linux (GNU coreutils). Nothing leaves the machine: every repository is a local bare one under $R, git
# reads an empty global config, GIT_ALLOW_PROTOCOL=file makes any accidental https/ssh clone fail, and a `gh` that exits
# 97 stands first on PATH.
R=$(mktemp -d); mkdir $R/bin; printf '#!/bin/sh\nexit 97\n' > $R/bin/gh; chmod +x $R/bin/gh; touch $R/gitconfig
export PATH=$R/bin:$PATH GIT_CONFIG_GLOBAL=$R/gitconfig GIT_CONFIG_NOSYSTEM=1 GIT_ALLOW_PROTOCOL=file \
       GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t.invalid
python3 $U/scaffold-project.py --org testorg --project Atlas --elected-by 'Test Human' --elected-on 2026-10-12 \
    --local-remote-dir $R/remotes --work-dir $R/work >/dev/null
python3 $U/scripts/family.py init --org testorg --family Fam --created-by 'Test Human' --created-on 2026-10-12 \
    --local-remote-dir $R/fam --work-dir $R/h >/dev/null
python3 $U/scripts/family.py add --family-root $R/h/Fam --member testorg/Atlas --local-remote-dir $R/remotes >/dev/null
git clone -q --recurse-submodules $R/remotes/Atlas.git $R/Atlas
mkdir $R/nogit && ln -s "$(command -v python3)" $R/nogit/python3      # a PATH with python3 on it and no git
last() { awk '/^Traceback/ {t = ""} {t = t $0 "\n"} END {printf "%s", t}' "$1" | grep -E '^  File "|^[A-Za-z]*Error' \
         | grep -v subprocess.py | sed -E 's|"[^"]*/scripts/|"scripts/|'; }       # the last traceback's frames, trimmed

echo "(1) the assembly root"; cd $R/Atlas
python3 scripts/bootstrap.py | tail -1; echo "bootstrap.py, git on PATH: exit ${PIPESTATUS[0]}"
env PATH=$R/nogit python3 scripts/bootstrap.py > $R/1.txt 2>&1; echo "bootstrap.py, no git: exit $?"; last $R/1.txt
echo "(2) a clone with no submodules yet, and a checkout of the standard named by SHAPE_UPSTREAM_PATH"
git clone -q $R/remotes/Atlas.git $R/Atlas2; cd $R/Atlas2
env PATH=$R/nogit SHAPE_UPSTREAM_PATH=$U python3 scripts/bootstrap.py > $R/2.txt 2>&1; echo "bootstrap.py, no git: exit $?"; last $R/2.txt
echo "(3) the family holder"; cd $R/h/Fam
env PATH=$R/nogit python3 scripts/bootstrap.py > $R/3.txt 2>&1; echo "bootstrap.py, no git: exit $?"; last $R/3.txt
env PATH=$R/nogit python3 scripts/bootstrap.py --no-fetch 2>&1 | tail -1; echo "bootstrap.py --no-fetch, no git: exit ${PIPESTATUS[0]}"

Output at dd8d0ea:

(1) the assembly root
bootstrap ok
bootstrap.py, git on PATH: exit 0
bootstrap.py, no git: exit 1
  File "scripts/bootstrap.py", line 420, in <module>
  File "scripts/bootstrap.py", line 403, in main
  File "scripts/bootstrap.py", line 349, in _checkout_all_tracking_branches
  File "scripts/bootstrap.py", line 114, in checkout_tracking_branch
  File "scripts/repo_shape.py", line 1055, in recorded_gitlink
  File "scripts/repo_shape.py", line 872, in git_out
FileNotFoundError: [Errno 2] No such file or directory: 'git'
(2) a clone with no submodules yet, and a checkout of the standard named by SHAPE_UPSTREAM_PATH
bootstrap.py, no git: exit 1
  File "scripts/bootstrap.py", line 420, in <module>
  File "scripts/bootstrap.py", line 406, in main
  File "scripts/bootstrap.py", line 376, in _run_neutral_validators
  File "scripts/bootstrap.py", line 175, in shape_upstream_notice
  File "scripts/bootstrap.py", line 95, in _try_git
  File "scripts/repo_shape.py", line 872, in git_out
FileNotFoundError: [Errno 2] No such file or directory: 'git'
(3) the family holder
bootstrap.py, no git: exit 1
  File "scripts/bootstrap.py", line 210, in <module>
  File "scripts/bootstrap.py", line 186, in main
  File "scripts/bootstrap.py", line 91, in fetch_members
FileNotFoundError: [Errno 2] No such file or directory: 'git'
family bootstrap ok
bootstrap.py --no-fetch, no git: exit 0

Why.

What it should do. Bootstrap says what is missing and never ends in a traceback. The decisions are the author's:

  1. Where.
  2. The code and the sentence. The one the two validators use, git-not-runnable (validate-family.py's default and --pins modes end in a FileNotFoundError traceback, exit 1 (a finding's code), when git is not on PATH: on main for every holder with a member (recorded_gitlink through repo_shape.git_out), and after #317 still for a holder whose .gitmodules is gone, though #317's own check answers gitmodules-unreadable, exit 2, whenever the file is there #368, The assembly root's scripts/validate-pins.py ends in a FileNotFoundError traceback, exit 1, a finding's code, when git is not on PATH, because recorded_gitlink reaches repo_shape.git_out and nothing catches OSError, where the file's own exit codes give 1 to drift and 2 to a question that could not be asked: the sibling of #368, whose fix is ruled into validate-family.py only #412, and the shared-wrapper option the review of The assembly root's scripts/validate-pins.py ends in a FileNotFoundError traceback, exit 1, a finding's code, when git is not on PATH, because recorded_gitlink reaches repo_shape.git_out and nothing catches OSError, where the file's own exit codes give 1 to drift and 2 to a question that could not be asked: the sibling of #368, whose fix is ruled into validate-family.py only #412 raised: one wrapper in scripts/repo_shape.py, which bootstrap.py already imports from, with the remediation's subject as a parameter), so that a project says one thing about a missing git whichever script meets it first.
  3. The notice. shape_upstream_notice is silent where it cannot answer. A _try_git that returns None for an OSError as it does for a Refusal makes it so.
  4. The holder. Whether a git that does not start is the same FETCH INCOMPLETE path (go on to each member's own make bootstrap, as the module's design has it for a git that fails) or a refusal.

How to test. In process, as tests/test_validate_pins_without_git.py does in PR #446: patch the module's git runner (git_out, and the holder's subprocess.run) to raise FileNotFoundError, change no PATH, so the test means the same on every OS. Assert the exit code and that no traceback is printed, once for step (a) with an initialised leg, once for the notice with SHAPE_UPSTREAM_PATH set, and once for the holder's fetch. One test may run the real subprocess with a PATH that holds no git and skips on Windows. Control: git runnable, exit 0, bootstrap ok, unchanged.

Verified at main dd8d0ea (the three sha256 above).

Related: #368 (validate-family.py with no git), #412 (validate-pins.py, PR #446), #391 (the manifest's name printed with a plain print in both of these files; PR #456 edits main beside this), #455 (a lone surrogate in tracking_branch reaches the same git_out and ends the same step in a traceback: another input that git_out cannot carry), #408 (the doctor's reading of a missing git; see its decision 2).

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    Lane: openreposhape-4 (openRepoShape-4)

    Moved to #459's known-limits checklist (#459 (comment)). Brett Heap ruled on 2026-10-12 that a new edge case found in review goes on its area's checklist, not into a new issue, and this lane filed this one before reading that ruling. Running bootstrap with no git on PATH is outside v1.0.0's scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions