Skip to content

scaffold-project.py: pin_digest_from_source catches only Refusal, so a --pin-source that does not exist, is a file, or sits under a parent this user cannot enter ends --dry-run and the real run alike in a FileNotFoundError, NotADirectoryError or PermissionError traceback (exit 1), where a directory that is not a clone is the pin-source-unreadable refusal (exit 2) with its remediation #339

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Named by this lane's writer of the #295 fix (PR #312, head eb0658f) as a follow-up: that fix turned the same crash into a finding in templates/assembly-root/scripts/validate-pins.py and left the scaffold's copy alone. The lane's handoff records it (addendum 17); neither review of #312 (eb0658f, 4aae417) nor the reviews of #288 carry it. Re-verified on main c7c02fa with scaffold-project.py run against local bare remotes under Python 3.12.3. gh was a shim that logs and exits 97 and was never called, because every run names --pin-source. #309, the one open PR that touches scaffold-project.py, changes three help= strings and the pin-collision remediation, not this path, so this survives the landing of lane 4's open PRs. Not claimed:

  • Windows.
  • Python versions other than 3.12.3. The crash is subprocess.run(cwd=...) raising before git starts, not pathlib, so it does not depend on 3.14.
  • The same call reached through setup.sh ... -- --pin-source <dir>. setup.sh hands it to this scaffold's --dry-run at the plan; read, not run.

What is wrong. declared_pin_values (scaffold-project.py:348-372 at c7c02fa) computes each --pin's digest through pin_digest_from_source(args.pin_source.resolve(), ...) (:123-134). That function calls repo_shape.tree_digest, which runs git ls-tree through git_out with cwd= the source. It catches Refusal only. A source git can enter but that is not a clone is git's own failure, a Refusal, and becomes pin-source-unreadable with a remediation. A source subprocess.run cannot enter at all raises OSError before git starts. Nothing catches that: main catches Refusal and CommandFailed (:338-345).

# $U: a clean checkout at c7c02fa. `gh` is never reached: every run names --pin-source and --local-remote-dir.
R=$(mktemp -d)
cd "$R" && mkdir plain locked locked/x && : > afile && chmod 000 locked
SHA=$(git -C "$U" rev-parse HEAD)          # any 40-hex commit will do: no source below is ever read
scaffold() {
    python3 "$U/scaffold-project.py" --org testorg --project MedxGlass --elected-by 'Test Human' \
        --local-remote-dir "$R/remotes" --work-dir "$R/work" --pin "openGlass@$SHA" "$@" 2>&1 | tail -n 2
    echo "exit ${PIPESTATUS[0]}"
}
scaffold --pin-source "$R/plain" --dry-run      # a directory that is not a clone
#   REFUSED pin-source-unreadable: $R/plain could not answer for opensoft/openGlass @ c7c02fa2...: `git ls-tree -r -z c7c02fa2...` in $R/plain exited 128: fatal: not a git repository ...
#   Remediation: point --pin-source at a clone of that repository that HAS the pinned commit (`git fetch --all` first), or drop --pin-source and let `gh api` read the forge.
#   exit 2
scaffold --pin-source "$R/missing" --dry-run    # no such directory
#       raise child_exception_type(errno_num, err_msg, err_filename)
#   FileNotFoundError: [Errno 2] No such file or directory: '$R/missing'
#   exit 1
scaffold --pin-source "$R/afile" --dry-run      # a file
#   NotADirectoryError: [Errno 20] Not a directory: '$R/afile'
#   exit 1
scaffold --pin-source "$R/locked/x" --dry-run   # under a parent this user cannot enter
#   PermissionError: [Errno 13] Permission denied: '$R/locked/x'
#   exit 1
scaffold --pin-source "$R/missing"              # the real run
#   FileNotFoundError: [Errno 2] No such file or directory: '$R/missing'
#   exit 1
chmod 755 locked && ls -A "$R"
#   afile  locked  plain                           (no remotes/, no work/: nothing was created)

The whole traceback runs main → _scaffold → _declaration → declared_pin_values → pin_digest_from_source → repo_shape.tree_digest → git_out → subprocess.run. It comes at the plan, before anything is created, so no work is lost. What is lost is the refusal: an operator who mistyped the path reads a Python traceback with Python's exit 1, not the refusal's exit 2, instead of the pin-source-unreadable sentence that names the fix. Through setup.sh it lands in the middle of the plan block, before the prompt.

A related silence: --pin-source "$R/missing" with NO --pin (and no --referent-chain) is never read at all. --dry-run says nothing was created. and exits 0 (checked).

Why. pin_digest_from_source was written for the one way a clone can fail to answer: git says so. A path that is not a directory never reaches git, and tree_digest's docstring promises nothing about it. #295 found the same gap in the project's copied validate-pins.py (_digest_from_source, "only Refusal is caught"). Its fix (PR #312) judges the source with one stat() inside a guard (FileNotFoundError/NotADirectoryError → "no such directory", any other OSError → its strerror, S_ISDIR for a file) and catches the OSError from git's cwd= too. The scaffold's copy was out of that issue's scope.

What it should do. Every way an explicit --pin-source fails to answer is pin-source-unreadable, exit 2, with the reason and the remediation, before anything is created. The decisions are the author's:

  1. How. Either catch OSError around tree_digest in pin_digest_from_source, with the system's reason in the detail, or judge the path first with validate-pins.py: a --pin-source value is PRODUCT=PATH only when the text before its first = names a declared neutral-product pin, so a bare stand-in under a=b/ recomputes the digest instead of falling to gh api and a skip, an explicit source that does not exist or is not a directory is the neutral-pin-source-unreadable finding instead of a FileNotFoundError traceback, and a value no pin used is noted (#295) #312's stat()-in-a-guard. The latter says "no such directory" for a missing path and "it is a file, not a directory" for a file, and still needs the OSError catch for a source that stat() can read but cwd= cannot enter.
  2. Whether the two copies share it. The scaffold imports repo_shape from this checkout, so it can import a helper the template's validate-pins.py cannot (that file imports only from the project's own pinned repo_shape.py). Either keep two small guards, each with a pointer to the other, or move one into repo_shape.py for the scaffold and leave the template its own.
  3. An unused --pin-source. Decide whether a --pin-source that no --pin and no --referent-chain reads is refused or noted (it is a flag that did nothing), as templates/assembly-root/scripts/validate-pins.py: a bare --pin-source <path> whose path holds an = is read as <product>=<path> (raw.partition("=")), so a stand-in clone under a directory named a=b is never consulted and the pin's digest silently goes to gh api and a skip (pins ok, exit 0), or, when the text before the = is a declared pin's name, to a FileNotFoundError traceback #295 decision 2 decided for the validator, or left as it is. Its --help calls it "the TEST path; no network".

How to test. In tests/test_scaffold_pin_and_reuse.py's style (local remotes, --pin-source, a gh that fails if called):

Verified at main c7c02fa (scaffold-project.py sha256 afef9917…, scripts/repo_shape.py c4c33094…); #309 (head 2ea1717) does not touch this path.

Related:

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session 3c13126a-509b-418f-b102-a3a282837b54@Eagle, 2026-10-12T14:30:41Z, for #339

    Logged in lanes/log/openRepoShape-5.md at bdc98ef87cbf97a13ebb51882e0c60333191d303.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#339:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#339"`, kept where the row or its body names `#339`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `339` as a whole token:
       none
    
  2. added 5 commits that reference this issue on Oct 13, 2026
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