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
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -973,20 +973,20 @@ python3 scripts/bump-leg.py --root <path-to-the-project> --leg spec \
It fetches the commit into the leg, checks the leg out AT it, recomputes the
`sorted-ls-tree-r-v1` digest from the leg's own objects, rewrites `commit:`
and `digests.tree_sha256` in `contracts/<role>-pin.yaml`, rewrites every
`.github/workflows/*.yml` `@<sha>` naming THAT leg's repository — and none
naming anything else, `actions/checkout@<sha>` and the sibling leg included —
then runs the project's own `validate-pins.py` and `validate-manifest.py` and
commits ONCE with explicit pathspecs.

It REFUSES a `--to` that is not exactly 40 hex, a commit no branch of the
leg's remote contains (including one committed inside the mount and never
pushed — a pin the rest of the world cannot fetch is a root it cannot
bootstrap), a dirty root, a root standing on its own tracking branch (the next
step is a pull request, never a push to the default branch), and a validator
that goes red — rolling the pin, the workflow files, the leg's checkout and
the index back first, so a refusal leaves the tree exactly as it was found.
The leg is left DETACHED at the new commit; `make bootstrap` re-places it on
its tracking branch AT the new pin.
TRACKED `.github/workflows/*.yml` `@<sha>` naming THAT leg's repository — and
none naming anything else, `actions/checkout@<sha>` and the sibling leg
included — then runs the project's own `validate-pins.py` and
`validate-manifest.py` and commits ONCE with explicit pathspecs.

It REFUSES a `--to` that is not exactly 40 hex, a commit no branch of the leg's
remote contains (including one committed inside the mount and never pushed — a
pin the rest of the world cannot fetch is a root it cannot bootstrap), a dirty
root, an untracked or ignored workflow file it would rewrite, a root standing on
its own tracking branch (the next step is a pull request, never a push to the
default branch), and a validator that goes red — rolling the pin, the workflow
files, the leg's checkout and the index back first, so a refusal leaves the tree
exactly as it was found. The leg is left DETACHED at the new commit; `make
bootstrap` re-places it on its tracking branch AT the new pin.

## Bootstrap is COPIED into the project, not fetched

Expand Down
20 changes: 11 additions & 9 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -1304,17 +1304,18 @@ <h3>Advancing a leg</h3>
--to &lt;40 hex commit&gt; # --dry-run prints the move and writes nothing</code></pre></div>
<p>It fetches the commit into the leg, checks the leg out AT it, recomputes the
<code>sorted-ls-tree-r-v1</code> digest from the leg&rsquo;s own objects, rewrites
<code>commit:</code> and <code>digests.tree_sha256</code>, rewrites every workflow
<code>commit:</code> and <code>digests.tree_sha256</code>, rewrites every TRACKED workflow
<code>@&lt;sha&gt;</code> naming THAT leg&rsquo;s repository &mdash; and none naming anything
else, <code>actions/checkout@&lt;sha&gt;</code> and the sibling leg included &mdash; then runs
the project&rsquo;s own <code>validate-pins.py</code> and <code>validate-manifest.py</code> and
commits ONCE with explicit pathspecs.</p>
<p><span class="refuses">Refuses</span> a <code>--to</code> that is not exactly 40 hex; a
commit no branch of the leg&rsquo;s remote contains (a pin the rest of the world cannot fetch
is a root it cannot bootstrap); a dirty root; a root standing on its own tracking branch (the
next step is a pull request, never a push to the default branch); and a validator that goes
red &mdash; rolling the pin, the workflow files, the leg&rsquo;s checkout and the index back
first, so a refusal leaves the tree exactly as it was found.</p>
is a root it cannot bootstrap); a dirty root; an untracked or ignored workflow file it would
rewrite; a root standing on its own tracking branch (the next step is a pull request, never a
push to the default branch); and a validator that goes red &mdash; rolling the pin, the
workflow files, the leg&rsquo;s checkout and the index back first, so a refusal leaves the
tree exactly as it was found.</p>
<p>The leg is left DETACHED at the new commit; <code>make bootstrap</code> re-places it on its
tracking branch AT the new pin. Editing the pin file by hand to make the validator agree is
what the validator exists to refuse.</p>
Expand Down Expand Up @@ -1780,10 +1781,11 @@ <h2>For AI assistants</h2>
<code>single-repository.yaml</code>, the doctor&rsquo;s <code>advisory</code> row and the
assistant&rsquo;s &ldquo;what you do tell them&rdquo; added (#163); a
<code>family.yaml</code> beside <code>project.yaml</code> as a <strong>manifest kinds</strong>
finding added (#211) &mdash; a page cannot name the commit that contains it, and the blob
below is the fact a test checks. README.md blob 20bb4a3e884c3232380407bda345af6e29d428c6.
Every command, flag, exit code and file name on this page is quoted from that README and
from AGENTS.md.</p>
finding added (#211); <code>bump-leg.py</code> rewriting tracked workflow files only, and
refusing an untracked or ignored one it would rewrite, added (#207) &mdash; a page cannot
name the commit that contains it, and the blob below is the fact a test checks.
README.md blob 23b0c6a55802788ce0113c00ea476714c4f38252. Every command, flag, exit code
and file name on this page is quoted from that README and from AGENTS.md.</p>
</footer>
</main>
</div>
Expand Down
78 changes: 71 additions & 7 deletions scripts/bump-leg.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

1. the GITLINK the assembly root records at the leg's path,
2. `commit:` and `digests.tree_sha256` in `contracts/<role>-pin.yaml`,
3. every `.github/workflows/*.yml` reference of the form
3. every TRACKED `.github/workflows/*.yml` reference of the form
`<owner>/<leg>/...@<40 hex>` naming THAT leg's repository.

`family.py bump` already did exactly this for a family's MEMBER pins. Nothing
Expand All @@ -33,6 +33,15 @@
* a dirty root. The commit is made with explicit pathspecs, so unrelated
edits would not be swept in — but a lockstep commit that cannot be read as
exactly one change is the thing the rule exists to keep readable.
* a workflow file git does not TRACK — untracked, or ignored — that names
the leg at any other commit. The lockstep commit's `git add` names every
file this rewrites: an untracked one would be added to the commit whole,
as a new file nobody committed, and an ignored one would stop that
`git add` after the rewrite, in git's own words. Leaving either alone
would turn the bump red, because `validate-pins.py` reads every file on
disk and would still see its old `@<sha>`. Refused by name, before
anything is written, under `--dry-run` as well. One that names no leg,
or this leg already at `--to`, is left alone.
* the root standing on its own tracking branch. The next step this prints
would then be a push to the default branch, and these organisations are
pull-request only.
Expand Down Expand Up @@ -228,9 +237,24 @@
return "\n".join(out) + "\n"


def _tracked_workflows(root: Path, workflows: Path) -> set[str]:
"""The paths git TRACKS under `workflows`, as POSIX paths relative to
`root` — the spelling `plan_workflows` and the commit's pathspecs use.

The index, by `git ls-files`: `_ensure_root_clean` has already refused a
root whose tracked files differ from it, so it is also what HEAD holds.
NUL-separated, so a name git would otherwise quote comes back as the
bytes it is.
"""
raw = git_out(["ls-files", "-z", "--",
workflows.relative_to(root).as_posix()],
cwd=root, binary=True)
return {os.fsdecode(name) for name in raw.split(b"\0") if name}


def plan_workflows(root: Path, repository: str,

Check failure on line 255 in scripts/bump-leg.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaEsU8zfBGKHLyXPfHdN&open=AaEsU8zfBGKHLyXPfHdN&pullRequest=222
commit: str) -> list[tuple[Path, str, int]]:
"""Every workflow file whose `@<sha>` refs to THIS leg have to move.
"""Every TRACKED workflow file whose `@<sha>` refs to THIS leg have to move.

Returns `(path, rewritten text, references moved)`, one row per file that
actually changes. A reference to any OTHER repository is left exactly as
Expand All @@ -240,11 +264,29 @@

`.yaml` is read as well as `.yml` because `validate-pins.py` reads both,
and a file this rewrote nothing in is a file the gate still calls red.

TRACKED FILES ONLY (#207). The directory is walked on disk, because that
is the set `validate-pins.py` reads, but a row is made for a file only
when git tracks it: the lockstep commit's `git add` names every file it
rewrote, so a draft that was never committed would be added to "Bump spec
leg" whole, as a new file, while `_ensure_root_clean` — which skips
untracked files on exactly the ground that they cannot reach this commit
— said nothing. (An IGNORED one is no better, only later: `git add`
refuses it after the rewrite, with its own hint and no word about this
tool.) An untracked or ignored file that has a reference to move is
REFUSED by name rather than skipped: skipped, `validate-pins.py` would
still read its old `@<sha>` off the disk and turn the bump red after the
rewrite, with a finding that never says the file is not in the
repository. One with nothing to move — no reference to this leg, or this
leg already at `commit` — is not this tool's business and is left alone,
unnamed.
"""
plans: list[tuple[Path, str, int]] = []
workflows = root / ".github" / "workflows"
if not workflows.is_dir():
return plans
tracked = _tracked_workflows(root, workflows)
untracked: list[tuple[str, int]] = []
for path in sorted(workflows.iterdir()):
if path.suffix not in (".yml", ".yaml") or not path.is_file():
continue
Expand All @@ -262,8 +304,28 @@

text = path.read_text(encoding="utf-8")
rewritten = WORKFLOW_REF_RE.sub(swap, text)
if moved:
if not moved:
continue
relative = path.relative_to(root).as_posix()
if relative in tracked:
plans.append((path, rewritten, moved))
else:
untracked.append((relative, moved))
if untracked:
raise Refusal(
"bump-leg-workflow-untracked",
f"{len(untracked)} workflow file(s) that git does not track "
f"(untracked or ignored) name {repository} at a commit other "
f"than {commit[:12]}:\n"
+ "\n".join(f" {relative} {count} reference(s)"
for relative, count in untracked),
"Remediation: the lockstep commit adds no file the repository "
"does not already hold, and `scripts/validate-pins.py` reads "
"every workflow file on disk, so one left where it is would turn "
"the bump red over its old `@<sha>`. Either commit it first — it "
"is then tracked and moves with the bump, and the dry run lists "
"it — or move it out of .github/workflows/ until the bump has "
"landed, then re-run.")
return plans


Expand Down Expand Up @@ -484,10 +546,12 @@
# THE ROOT MUST BE CLEAN. Tracked changes only: an untracked scratch file
# cannot reach a commit made with explicit pathspecs, and refusing one
# would be this tool having an opinion about somebody's working
# directory. Untracked content INSIDE the leg is ignored for the same
# reason; the leg sitting at a different commit than the gitlink is NOT,
# because that is a half-finished bump and this one would land on top of
# it.
# directory. The one kind that could reach it, a workflow file the rewrite
# would pick up, is never picked up: `plan_workflows` rewrites tracked
# files only and refuses an untracked one by name (#207). Untracked
# content INSIDE the leg is ignored for the same reason; the leg sitting
# at a different commit than the gitlink is NOT, because that is a
# half-finished bump and this one would land on top of it.
dirt = git_out(["status", "--porcelain", "--untracked-files=no",
"--ignore-submodules=untracked"], cwd=root)
if dirt:
Expand Down
Loading
Loading