mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
Installer teaches an existing pre-commit workflow where the shared gate lives
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da7fa911b2
commit
d6b92cfd18
@ -1,11 +1,12 @@
|
||||
"""Install the three gate pieces into a repo, from one set of templates.
|
||||
"""Install the gate's pieces into a repo, from one set of templates.
|
||||
|
||||
The gate ships as three hand-replicated files per repo -- a delegate script, a
|
||||
pre-commit hook entry and a CI workflow -- and hand-replication is exactly how
|
||||
the 250-line cap ended up with four different hook ids, two of them
|
||||
independent reimplementations rather than delegates. Sixteen more repos of
|
||||
copy-paste would repeat that, so the copy is a script and the templates have
|
||||
one home.
|
||||
pre-commit hook entry and a CI workflow, plus a fourth edit to an existing
|
||||
`pre-commit` workflow where one exists (see install_precommit_ci) -- and
|
||||
hand-replication is exactly how the 250-line cap ended up with four different
|
||||
hook ids, two of them independent reimplementations rather than delegates.
|
||||
Sixteen more repos of copy-paste would repeat that, so the copy is a script
|
||||
and the templates have one home.
|
||||
|
||||
Idempotent by design: running it twice changes nothing the second time, and
|
||||
`plan()` reports what a run WOULD do without touching the repo.
|
||||
@ -15,6 +16,12 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dep_freshness.install_precommit_ci import (
|
||||
PRECOMMIT_WORKFLOW,
|
||||
needs_patch,
|
||||
patch,
|
||||
)
|
||||
|
||||
TEMPLATES = Path(__file__).resolve().parent / "templates"
|
||||
DELEGATE = Path("scripts/check_dependency_freshness.sh")
|
||||
WORKFLOW = Path(".github/workflows/dependency-freshness.yml")
|
||||
@ -65,8 +72,7 @@ def _local_block_end(lines: list[str]) -> int:
|
||||
if not starts:
|
||||
return -1
|
||||
after = [
|
||||
n for n in range(starts[-1] + 1, len(lines))
|
||||
if lines[n].startswith(" - repo:")
|
||||
n for n in range(starts[-1] + 1, len(lines)) if lines[n].startswith(" - repo:")
|
||||
]
|
||||
return after[0] if after else len(lines)
|
||||
|
||||
@ -106,6 +112,8 @@ def plan(repo: Path) -> list[str]:
|
||||
todo.append(str(WORKFLOW))
|
||||
if _needs_hook(repo):
|
||||
todo.append(f"{PRECOMMIT} ({HOOK_ID} hook)")
|
||||
if needs_patch(repo):
|
||||
todo.append(f"{PRECOMMIT_WORKFLOW} (shared-gate checkout)")
|
||||
return todo
|
||||
|
||||
|
||||
@ -126,6 +134,8 @@ def install(repo: Path) -> list[str]:
|
||||
if _needs_hook(repo):
|
||||
_write_hook(repo)
|
||||
done.append(f"{PRECOMMIT} ({HOOK_ID} hook)")
|
||||
if patch(repo):
|
||||
done.append(f"{PRECOMMIT_WORKFLOW} (shared-gate checkout)")
|
||||
return done
|
||||
|
||||
|
||||
|
||||
67
dep_freshness/install_precommit_ci.py
Normal file
67
dep_freshness/install_precommit_ci.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Give a repo's `pre-commit` CI workflow the shared-gate checkout it needs.
|
||||
|
||||
The fourth piece of the install, and the one that is easy to forget because
|
||||
nothing local reveals it: `.pre-commit-config.yaml` gains a hook that execs
|
||||
`scripts/check_dependency_freshness.sh`, which delegates to `~/utils`. That
|
||||
path exists on this machine and does not exist on a runner, so a repo whose
|
||||
`pre-commit` workflow runs `pre-commit/action` against a bare checkout goes red
|
||||
with "shared gate not found at /home/runner/utils/..." -- green locally, red in
|
||||
CI, with no diff that explains it. screen-locker, wake-alarm and diet-guard
|
||||
each hit it and each got the same twelve lines pasted in by hand.
|
||||
|
||||
Only repos that HAVE such a workflow are touched; the dedicated
|
||||
`dependency-freshness.yml` already carries its own checkout.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
PRECOMMIT_WORKFLOW = Path(".github/workflows/pre-commit.yml")
|
||||
MARKER = "UTILS_ROOT"
|
||||
CHECKOUT = "- uses: actions/checkout@v4"
|
||||
|
||||
PATCH = """\
|
||||
|
||||
# Hooks here delegate to the shared gates in kuhyx/utils. Without this
|
||||
# checkout they exit 1 with "shared gate not found at
|
||||
# /home/runner/utils/..." -- green locally, red on every runner.
|
||||
- name: Check out the shared gates
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kuhyx/utils
|
||||
path: .utils
|
||||
- name: Point the delegates at that checkout
|
||||
run: echo "UTILS_ROOT=$GITHUB_WORKSPACE/.utils" >> "$GITHUB_ENV"\
|
||||
"""
|
||||
|
||||
|
||||
def needs_patch(repo: Path) -> bool:
|
||||
"""True when a pre-commit workflow exists and cannot find the shared gate."""
|
||||
target = repo / PRECOMMIT_WORKFLOW
|
||||
if not target.is_file():
|
||||
return False
|
||||
return MARKER not in target.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def patch(repo: Path) -> bool:
|
||||
"""Insert the checkout after the workflow's own `actions/checkout`.
|
||||
|
||||
Inserting after the repo's own checkout rather than at the top of `steps:`
|
||||
keeps `$GITHUB_WORKSPACE` populated before `.utils` lands inside it, and
|
||||
leaves every surrounding comment intact -- a YAML round-trip would drop
|
||||
the ones explaining why each hook is there.
|
||||
"""
|
||||
if not needs_patch(repo):
|
||||
return False
|
||||
target = repo / PRECOMMIT_WORKFLOW
|
||||
lines = target.read_text(encoding="utf-8").split("\n")
|
||||
anchors = [n for n, line in enumerate(lines) if line.strip() == CHECKOUT]
|
||||
if not anchors:
|
||||
raise ValueError(
|
||||
f"{target} has no `{CHECKOUT}` step to insert after; "
|
||||
"add the shared-gate checkout by hand, then re-run"
|
||||
)
|
||||
lines[anchors[0] + 1 : anchors[0] + 1] = PATCH.split("\n")
|
||||
target.write_text("\n".join(lines), encoding="utf-8")
|
||||
return True
|
||||
91
dep_freshness/tests/test_install_precommit_ci.py
Normal file
91
dep_freshness/tests/test_install_precommit_ci.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""The fourth install piece: teaching an existing pre-commit workflow the gate.
|
||||
|
||||
These assertions are about a failure that is invisible locally -- the delegate
|
||||
resolves through `~/utils` on this machine and through nothing at all on a
|
||||
runner -- so they check the runner-visible facts: that the shared repo is
|
||||
checked out, that `UTILS_ROOT` points at it, and that the insert lands after
|
||||
the workflow's own checkout rather than before it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from dep_freshness import install as installer
|
||||
from dep_freshness import install_precommit_ci as ci
|
||||
from dep_freshness.tests.conftest import write
|
||||
|
||||
BARE_WORKFLOW = """\
|
||||
name: pre-commit
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
"""
|
||||
|
||||
|
||||
def test_a_repo_with_no_precommit_workflow_is_left_alone(repo):
|
||||
assert ci.needs_patch(repo) is False
|
||||
assert ci.patch(repo) is False
|
||||
assert not (repo / ci.PRECOMMIT_WORKFLOW).exists()
|
||||
|
||||
|
||||
def test_a_bare_workflow_gains_the_checkout_and_the_env_var(repo):
|
||||
write(repo, str(ci.PRECOMMIT_WORKFLOW), BARE_WORKFLOW)
|
||||
|
||||
assert ci.needs_patch(repo) is True
|
||||
assert ci.patch(repo) is True
|
||||
|
||||
body = (repo / ci.PRECOMMIT_WORKFLOW).read_text(encoding="utf-8")
|
||||
assert "repository: kuhyx/utils" in body
|
||||
assert "path: .utils" in body
|
||||
assert "UTILS_ROOT=$GITHUB_WORKSPACE/.utils" in body
|
||||
|
||||
|
||||
def test_the_insert_lands_after_the_workflows_own_checkout(repo):
|
||||
write(repo, str(ci.PRECOMMIT_WORKFLOW), BARE_WORKFLOW)
|
||||
ci.patch(repo)
|
||||
|
||||
lines = (repo / ci.PRECOMMIT_WORKFLOW).read_text(encoding="utf-8").split("\n")
|
||||
own = lines.index(" - uses: actions/checkout@v4")
|
||||
shared = lines.index(" - name: Check out the shared gates")
|
||||
action = lines.index(" - uses: pre-commit/action@v3.0.1")
|
||||
assert own < shared < action, "$GITHUB_WORKSPACE must exist before .utils lands"
|
||||
|
||||
|
||||
def test_a_second_run_writes_nothing(repo):
|
||||
write(repo, str(ci.PRECOMMIT_WORKFLOW), BARE_WORKFLOW)
|
||||
ci.patch(repo)
|
||||
once = (repo / ci.PRECOMMIT_WORKFLOW).read_text(encoding="utf-8")
|
||||
|
||||
assert ci.needs_patch(repo) is False
|
||||
assert ci.patch(repo) is False
|
||||
assert (repo / ci.PRECOMMIT_WORKFLOW).read_text(encoding="utf-8") == once
|
||||
|
||||
|
||||
def test_a_workflow_with_no_checkout_refuses_rather_than_guessing(repo):
|
||||
write(
|
||||
repo,
|
||||
str(ci.PRECOMMIT_WORKFLOW),
|
||||
"name: pre-commit\njobs:\n pre-commit:\n steps:\n"
|
||||
" - uses: pre-commit/action@v3.0.1\n",
|
||||
)
|
||||
with pytest.raises(ValueError, match="no `- uses: actions/checkout@v4` step"):
|
||||
ci.patch(repo)
|
||||
|
||||
|
||||
def test_the_installer_reports_the_patch_as_a_fourth_piece(repo):
|
||||
write(repo, str(ci.PRECOMMIT_WORKFLOW), BARE_WORKFLOW)
|
||||
|
||||
expected = f"{ci.PRECOMMIT_WORKFLOW} (shared-gate checkout)"
|
||||
assert expected in installer.plan(repo)
|
||||
assert expected in installer.install(repo)
|
||||
assert installer.install(repo) == []
|
||||
Loading…
Reference in New Issue
Block a user