mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
A repo with `.dep-freshness-no-workflow` at its root keeps the local pieces (delegate script, pre-commit hook) but the installer no longer creates or rewrites `.github/workflows/dependency-freshness.yml` there. teatr-pw deleted its workflows on purpose (no GitHub Actions minutes on a private repo; its pre-push hook runs `--all --strict`), and without this every fleet sweep would put the workflow back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8tUQzqQmiidbg8ZXmeQAg
182 lines
6.6 KiB
Python
182 lines
6.6 KiB
Python
"""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, 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.
|
|
"""
|
|
|
|
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")
|
|
# Repo-root file that opts a repo out of WORKFLOW (see _needs_workflow).
|
|
NO_WORKFLOW_MARKER = Path(".dep-freshness-no-workflow")
|
|
PRECOMMIT = Path(".pre-commit-config.yaml")
|
|
HOOK_ID = "dependency-freshness"
|
|
|
|
EMPTY_PRECOMMIT = "repos:\n - repo: local\n hooks:\n"
|
|
|
|
|
|
def _template(name: str) -> str:
|
|
return (TEMPLATES / name).read_text(encoding="utf-8")
|
|
|
|
|
|
def _owns_the_shared_gate(repo: Path) -> bool:
|
|
"""True for utils itself, where DELEGATE is the real checker, not a shim.
|
|
|
|
Without this the installer "fixes drift" in the one repo whose copy is
|
|
supposed to differ, and the result execs itself: the delegate resolves
|
|
the shared gate to `$HOME/src/utils/scripts/check_dependency_freshness.sh`,
|
|
which is the file it just overwrote.
|
|
"""
|
|
return (repo / "dep_freshness" / "check.py").is_file()
|
|
|
|
|
|
def _needs_delegate(repo: Path) -> bool:
|
|
if _owns_the_shared_gate(repo):
|
|
return False
|
|
target = repo / DELEGATE
|
|
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
|
"delegate.sh"
|
|
)
|
|
|
|
|
|
def _needs_workflow(repo: Path) -> bool:
|
|
"""True when the CI workflow is missing or drifted -- unless opted out.
|
|
|
|
A repo with NO_WORKFLOW_MARKER at its root runs the gate locally only
|
|
(teatr-pw: no GitHub Actions minutes on a private repo, its pre-push hook
|
|
runs `--all --strict`). Without the opt-out every fleet sweep would put
|
|
back the workflow that repo deleted on purpose. The local pieces are
|
|
still installed.
|
|
"""
|
|
if (repo / NO_WORKFLOW_MARKER).exists():
|
|
return False
|
|
target = repo / WORKFLOW
|
|
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
|
"workflow.yml"
|
|
)
|
|
|
|
|
|
def _needs_hook(repo: Path) -> bool:
|
|
target = repo / PRECOMMIT
|
|
if not target.is_file():
|
|
return True
|
|
return f"id: {HOOK_ID}" not in target.read_text(encoding="utf-8")
|
|
|
|
|
|
LOCAL_REPO = " - repo: local"
|
|
|
|
|
|
def _local_block_end(lines: list[str]) -> int:
|
|
"""Index just past the LAST `- repo: local` block, or -1 if there is none.
|
|
|
|
Appending at end-of-file is wrong whenever a config interleaves local and
|
|
remote hook repos, which most of these do -- diet-guard, screen-locker and
|
|
wake-alarm each carry nine `- repo:` blocks. A block appended under
|
|
`- repo: https://github.com/...` would be handed to that remote repo's
|
|
hook definitions and fail with an unhelpful error about an unknown id.
|
|
"""
|
|
starts = [n for n, line in enumerate(lines) if line.rstrip() == LOCAL_REPO]
|
|
if not starts:
|
|
return -1
|
|
after = [
|
|
n for n in range(starts[-1] + 1, len(lines)) if lines[n].startswith(" - repo:")
|
|
]
|
|
return after[0] if after else len(lines)
|
|
|
|
|
|
def _write_hook(repo: Path) -> None:
|
|
"""Insert the hook into the repo's LAST `local` hooks list.
|
|
|
|
Inserting rather than parsing-and-re-emitting is deliberate: every one of
|
|
these configs carries comments explaining why a hook exists, and a YAML
|
|
round-trip through the standard library drops all of them.
|
|
"""
|
|
target = repo / PRECOMMIT
|
|
body = target.read_text(encoding="utf-8") if target.is_file() else EMPTY_PRECOMMIT
|
|
if not body.endswith("\n"):
|
|
body += "\n"
|
|
# `body` was just made to end in a newline, so the split always leaves a
|
|
# trailing empty element; dropping it keeps the join below symmetric.
|
|
lines = body.split("\n")[:-1]
|
|
cut = _local_block_end(lines)
|
|
if cut < 0:
|
|
raise ValueError(
|
|
f"{target} has no `{LOCAL_REPO}` block to add the hook to; "
|
|
"add one by hand, then re-run"
|
|
)
|
|
block = _template("precommit-hook.yaml").rstrip("\n").split("\n")
|
|
lines[cut:cut] = ["", *block]
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def plan(repo: Path) -> list[str]:
|
|
"""Which pieces are missing or have drifted from the template."""
|
|
todo = []
|
|
if _needs_delegate(repo):
|
|
todo.append(str(DELEGATE))
|
|
if _needs_workflow(repo):
|
|
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
|
|
|
|
|
|
def install(repo: Path) -> list[str]:
|
|
"""Write every missing or drifted piece. Returns what changed."""
|
|
done = []
|
|
if _needs_delegate(repo):
|
|
target = repo / DELEGATE
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("delegate.sh"), encoding="utf-8")
|
|
target.chmod(0o755)
|
|
done.append(str(DELEGATE))
|
|
if _needs_workflow(repo):
|
|
target = repo / WORKFLOW
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("workflow.yml"), encoding="utf-8")
|
|
done.append(str(WORKFLOW))
|
|
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
|
|
|
|
|
|
def write_fvmrc(repo: Path, version: str) -> bool:
|
|
"""Pin the Flutter SDK. Returns True if the file changed.
|
|
|
|
Only for repos that actually hold a pubspec.yaml -- an .fvmrc in a Python
|
|
repo is a declaration about a toolchain nothing there builds with, and the
|
|
gate would then check a version that cannot go stale in any useful sense.
|
|
"""
|
|
if not any(repo.rglob("pubspec.yaml")):
|
|
return False
|
|
target = repo / ".fvmrc"
|
|
body = '{\n "flutter": "%s"\n}\n' % version
|
|
if target.is_file() and target.read_text(encoding="utf-8") == body:
|
|
return False
|
|
target.write_text(body, encoding="utf-8")
|
|
return True
|