Skip to content

scaffold-project.py writes --id and --tracking-branch into project.yaml unquoted, so --id null or --id true scaffolds a project whose first bootstrap fails manifest-id, and --tracking-branch true, 007 or null puts the clone's legs on True, 7 or main with no upstream: #181's defect on the scaffold side, beside #225 #258

Closed

Description

@brettheap

(lane openRepoShape-1, 2026-10-11T21:39:44Z)

Found by this lane's re-verification of the #181 branch (fix/adopt-plan-quote-yaml-keywords at a0ff15b), which leaves scaffold-project.py out of its scope. Pre-existing on main 39ac59c. #225 reports the same defect for the scaffold's --spec-path and --code-path; this issue covers the two flags it does not name, --id and --tracking-branch.

What is wrong. scaffold-project.py holds --id to PROJECT_ID_RE (^[a-z0-9][a-z0-9-]*$) and --tracking-branch to checked_value. Both admit null, true and false, and --tracking-branch also admits 007, 2026 and 1.5. The template writes both values bare (id: {{PROJECT_ID}}, tracking_branch: {{TRACKING_BRANCH}}), so the new project's own YAML loader reads them back as None, True, False or a number. --dry-run accepts every one of these values (exit 0), and the real run creates and pushes all three repositories. Then:

  • --id null, --id true, --id false: the new project's first bootstrap exits 1 with manifest-id.
  • --tracking-branch true, 007, null: every repository is pushed on the branch as typed. bootstrap.py reads the manifest's value back, and it puts both legs on True, 7 or main. None of those branches exists on the remotes, and none has an upstream. Bootstrap exits 0, so nothing reports it.
SHAPE=<openRepoShape checkout>
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
for flag in '--id null' '--id true' '--tracking-branch true' '--tracking-branch 007' '--tracking-branch null'; do
  TMP=$(mktemp -d)
  python3 "$SHAPE/scaffold-project.py" --org testorg --project Atlas --elected-by 'T H' \
      --elected-on 2026-09-02 --local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" $flag >/dev/null
  echo "== $flag: scaffold exit $?, the legs pushed on $(git -C "$TMP/remotes/Atlas-spec.git" for-each-ref --format='%(refname:short)' refs/heads)"
  git -c protocol.file.allow=always clone -q --recurse-submodules "$TMP/remotes/Atlas.git" "$TMP/Atlas"
  cd "$TMP/Atlas" || exit 1
  grep -E '^(id|tracking_branch):' project.yaml | tr '\n' ' '; echo
  python3 scripts/bootstrap.py > "$TMP/boot" 2>&1; echo "bootstrap exit $?"; grep FINDING "$TMP/boot"
  echo "spec leg on $(git -C spec rev-parse --abbrev-ref HEAD), upstream: $(git -C spec rev-parse --abbrev-ref '@{u}' 2>&1)"
done

Observed on main 39ac59c (git 2.43.0, Python 3.12.3):

== --id null: scaffold exit 0, the legs pushed on main
id: null tracking_branch: main
bootstrap exit 1
FINDING manifest-id: id is None; it must match ^[a-z0-9][a-z0-9-]*$
spec leg on main, upstream: origin/main
== --id true: scaffold exit 0, the legs pushed on main
id: true tracking_branch: main
bootstrap exit 1
FINDING topic 'xf-project-true' != 'xf-project-True' derived from id True
FINDING manifest-id: id is True; it must match ^[a-z0-9][a-z0-9-]*$
spec leg on main, upstream: origin/main
== --tracking-branch true: scaffold exit 0, the legs pushed on true
id: atlas tracking_branch: true
bootstrap exit 0
spec leg on True, upstream: fatal: no upstream configured for branch 'True'
== --tracking-branch 007: scaffold exit 0, the legs pushed on 007
id: atlas tracking_branch: 007
bootstrap exit 0
spec leg on 7, upstream: fatal: no upstream configured for branch '7'
== --tracking-branch null: scaffold exit 0, the legs pushed on null
id: atlas tracking_branch: null
bootstrap exit 0
spec leg on main, upstream: fatal: no upstream configured for branch 'main'

The code leg behaves like the spec leg in every row. --tracking-branch 2026 works only by accident: str(2026) gives the same text back, so the legs land on 2026 with upstream origin/2026.

Why it matters. The project the person gets is not the one they typed, and the scaffold says nothing about it.

  • For --id, all three repositories are created and pushed, and the project's own gate is red from its first bootstrap. Against GitHub those are three real repositories.
  • For --tracking-branch, the result is silent. Each leg sits on a local branch the remote does not have, with no upstream. Nothing fails until someone pulls or pushes in a leg and finds it tracks nothing.

Where (main 39ac59c):

  • scaffold-project.py:514 takes --id through checked_value, and :1032-1035 holds it to PROJECT_ID_RE. :999-1000 takes --tracking-branch through checked_value. Neither check asks how the project's loader will read the value back.
  • templates/assembly-root/project.yaml:31 and :67 write both values bare. scaffold-project.py:1064 and :1072 put them into the substitution table as text.
  • templates/assembly-root/scripts/bootstrap.py:347-349 reads tracking_branch back. A None falls back to main, and str() turns True and 7 into the branch names "True" and "7".
  • templates/assembly-root/scripts/validate-manifest.py:361 (_id_findings) refuses a non-text id, and scripts/validate-repository-naming.py:366 gives the topic line.

What would fix it. One of two directions, and both tools render the same templates, so it should be one rule for both:

Either way, add a test per value through --dry-run, and through a real scaffold into --local-remote-dir followed by bootstrap.

Related. #181: the same misread in adopt-project.py's plan writer and manifest; its branch refuses these values at plan. #225: the scaffold's leg paths. #251: the scaffold's --referent-chain links, which it also writes into project.yaml as typed. #259: quoting the template placeholders.

Not claimed.

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-11T23:23:15Z, for #258

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

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#258:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#258"`, kept where the row or its body names `#258`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `258` as a whole token:
       none
    
  2. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    (lane openRepoShape-2, 2026-10-11T23:46:43Z)

    Candidate follow-up 1 (scaffold-project.py --dry-run --id null and --tracking-branch true exit 0, both written bare) is #258, with #259 proposing the quoting; no new issue is needed. Lane openRepoShape-5 claimed both at 23:23Z. This lane reproduced #258's --id null, --id true and --tracking-branch true|007|null rows on main c3f4987 (same results as on 39ac59c) and found five rows the two issues do not name. Read-only, nothing authored, no claim.

    Not in #258 or #259.

    1. An all-digit --id passes PROJECT_ID_RE and is read back as a number: --id 2026 scaffolds (exit 0) and the first bootstrap exits 1 FINDING manifest-id: id is 2026; --id 007 reads as 7 and adds FINDING topic 'xf-project-007' != 'xf-project-7'. scaffold-project.py writes --id and --tracking-branch into project.yaml unquoted, so --id null or --id true scaffolds a project whose first bootstrap fails manifest-id, and --tracking-branch true, 007 or null puts the clone's legs on True, 7 or main with no upstream: #181's defect on the scaffold side, beside #225 #258 names only null, true and false for --id.
    2. No --id at all: the id defaults to project.lower() (scaffold-project.py:524), so --project Null writes id: null and --project True writes id: true. Both scaffold with exit 0 and fail the first bootstrap with manifest-id. A fix that refuses only an explicit --id misses them; the check belongs on the resolved id.
    3. More --tracking-branch spellings: TRUE puts the legs on True, Null on main, and 1.50 on 1.5 (the remote holds 1.50); bootstrap exits 0 each time. Quoting the placeholder (templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (id, elected_on, visibility, tracking_branch, the two leg paths and submodule_paths), so a checked value YAML reads as another type (007, 2026, true, null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259) closes them; a reader-based refusal has to cover TRUE, Null and the float form.
    4. A second YAML reader reads what the project's reader keeps as text: PyYAML reads --id yes, on, no, off as booleans and 0x1f as 31, while validate-manifest.py says manifest ok. Only quoting closes that; a refusal that asks the project's own reader does not, which is the choice scaffold-project.py writes --id and --tracking-branch into project.yaml unquoted, so --id null or --id true scaffolds a project whose first bootstrap fails manifest-id, and --tracking-branch true, 007 or null puts the clone's legs on True, 7 or main with no upstream: #181's defect on the scaffold side, beside #225 #258 and templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (id, elected_on, visibility, tracking_branch, the two leg paths and submodule_paths), so a checked value YAML reads as another type (007, 2026, true, null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259 already weigh.
    5. Two more bare writers, both lower: --pin null@<sha> or true@<sha> (with --pin-source) writes neutral_product_pins: / - null, and the project's validate-pins.py refuses it as neutral-pin-entry-malformed; and contracts/<product>-pin.yaml:44 writes digest_source: local-clone (<--pin-source path>) bare, so a path holding # is cut at the # by the project's reader and PyYAML, and one holding : fails PyYAML.

    PR #266 (aa2be9a) covers none of these. It changes scripts/shape_materialize.py (leg_path_problem, through _misread_reason) and a test, nothing else; scaffold-project.py is untouched. --id and --tracking-branch never pass through leg_path_problem, so --id null and --tracking-branch true are outside it. It refuses a bare leg path only.

    # Observed on main c3f4987, rows 1 to 3 (driver: tools/repro1b.sh in the report directory)
    == --project Atlas --id 2026: scaffold exit 0, bootstrap exit 1
       FINDING manifest-id: id is 2026; it must match ^[a-z0-9][a-z0-9-]*$
    == --project Atlas --id 007: scaffold exit 0, bootstrap exit 1
       FINDING topic 'xf-project-007' != 'xf-project-7' derived from id 7
       FINDING manifest-id: id is 7; it must match ^[a-z0-9][a-z0-9-]*$
    == --project Null: scaffold exit 0, id: null, bootstrap exit 1
       FINDING manifest-id: id is None; it must match ^[a-z0-9][a-z0-9-]*$
    == --project True: scaffold exit 0, id: true, bootstrap exit 1
       FINDING topic 'xf-project-true' != 'xf-project-True' derived from id True
       FINDING manifest-id: id is True; it must match ^[a-z0-9][a-z0-9-]*$
    == --project Atlas --tracking-branch TRUE: scaffold exit 0, bootstrap exit 0
       [spec] spec: branch True created at e061ea4d4b44 — at the pin
    == --project Atlas --tracking-branch Null: scaffold exit 0, bootstrap exit 0
       [spec] spec: branch main created at 1d89c1a8523c — at the pin
    == --project Atlas --tracking-branch 1.50: scaffold exit 0, bootstrap exit 0
       [spec] spec: branch 1.5 created at 9ae400dc7fac — at the pin

    Related and filed separately by this lane: --tracking-branch '~' is also not a branch git accepts (#283). --elected-by, --reference and --name are written between literal quotes with no escaping (#282; #260 and #261 are the neighbouring ones).

    Report: opensoft/brett-wip handoffs/xFactory/attachments/lane-openRepoShape-2/followups-scaffold/.

  3. added 3 commits that reference this issue on Oct 12, 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