mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +01:00
Script the gate rollout instead of hand-copying it into sixteen repos
Some checks failed
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
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
design_system tests / test (push) Has been cancelled
sync_settings_ui tests / test (push) Has been cancelled
Some checks failed
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
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
design_system tests / test (push) Has been cancelled
sync_settings_ui tests / test (push) Has been cancelled
The gate is three files per repo -- a delegate, a pre-commit hook entry and a CI workflow -- and Phase 3 needs them in sixteen more repos. Hand-replication is precisely how the 250-line cap ended up shipping under four different hook ids, two of which were independent reimplementations rather than delegates, so the copy is now a script and the templates have exactly one home. scripts/install_dependency_freshness_gate.sh <repo> [--check] [--fvm] Idempotent, and it rewrites a copy that has drifted from the template rather than leaving it. `--check` reports without writing. `--fvm` pins the Flutter SDK, and only in a repo that actually holds a pubspec.yaml -- an .fvmrc in a Python repo declares a toolchain nothing there builds with. An unresolvable SDK version leaves the pin alone and exits 1: guessing would pin every repo to a number nobody chose. The pre-commit block is APPENDED rather than parsed and re-emitted. Every one of these configs carries comments explaining why its hooks exist, and a YAML round-trip through the standard library drops all of them. 306 tests, 100% branch coverage on every production module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
This commit is contained in:
parent
51700aa89e
commit
71a43d9658
113
dep_freshness/install.py
Normal file
113
dep_freshness/install.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Install the three gate 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.
|
||||
|
||||
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
|
||||
|
||||
TEMPLATES = Path(__file__).resolve().parent / "templates"
|
||||
DELEGATE = Path("scripts/check_dependency_freshness.sh")
|
||||
WORKFLOW = Path(".github/workflows/dependency-freshness.yml")
|
||||
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 _needs_delegate(repo: Path) -> bool:
|
||||
target = repo / DELEGATE
|
||||
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
||||
"delegate.sh"
|
||||
)
|
||||
|
||||
|
||||
def _needs_workflow(repo: Path) -> bool:
|
||||
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")
|
||||
|
||||
|
||||
def _write_hook(repo: Path) -> None:
|
||||
"""Append the hook block to the repo's `local` hooks list.
|
||||
|
||||
Appending 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"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(f"{body}\n{_template('precommit-hook.yaml')}", 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)")
|
||||
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)")
|
||||
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
|
||||
57
dep_freshness/install_cli.py
Normal file
57
dep_freshness/install_cli.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Command line for `install`: which repo, check-only, and the SDK pin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dep_freshness.install import install, plan, write_fvmrc
|
||||
from dep_freshness.registries.toolchain import flutter_latest
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Install the dependency-freshness gate into a repo."
|
||||
)
|
||||
parser.add_argument("repo", type=Path)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="report what would be written, change nothing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fvm", action="store_true", help="also pin the Flutter SDK in <repo>/.fvmrc"
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
repo = args.repo.resolve()
|
||||
|
||||
if args.check:
|
||||
todo = plan(repo)
|
||||
for piece in todo:
|
||||
print(f"would write: {piece}")
|
||||
if not todo:
|
||||
print("gate already current")
|
||||
return 0
|
||||
|
||||
for piece in install(repo):
|
||||
print(f"wrote: {piece}")
|
||||
|
||||
if args.fvm:
|
||||
version, _dart = flutter_latest()
|
||||
if version is None:
|
||||
# Guessing a version here would pin every repo to a number nobody
|
||||
# chose, and the gate would then compare against it forever.
|
||||
print(
|
||||
"could not resolve latest stable Flutter; .fvmrc left alone",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if write_fvmrc(repo, version):
|
||||
print(f"wrote: .fvmrc (flutter {version})")
|
||||
|
||||
return 0
|
||||
10
dep_freshness/install_main.py
Normal file
10
dep_freshness/install_main.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""`python3 -m dep_freshness.install_main` — the installer's entrypoint.
|
||||
|
||||
Three lines for the same reason as `__main__.py`: an `if __name__` guard
|
||||
inside install_cli.py would be a branch no test can enter, and the honest way
|
||||
to hold full coverage is to have no untestable branch rather than suppress one.
|
||||
"""
|
||||
|
||||
from dep_freshness.install_cli import main
|
||||
|
||||
raise SystemExit(main())
|
||||
34
dep_freshness/templates/delegate.sh
Executable file
34
dep_freshness/templates/delegate.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Fail if any dependency in the commit is behind its ecosystem's newest stable.
|
||||
#
|
||||
# Thin delegate to the shared gate in ~/utils, which owns the registry
|
||||
# adapters, the pre-release rules and the allowlist semantics. Copying that
|
||||
# logic here is what lets one repo's idea of "current" drift from every other
|
||||
# repo's -- so this script only locates the shared checker and forwards its
|
||||
# arguments.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/check_dependency_freshness.sh <file> [<file> ...] # pre-commit
|
||||
# scripts/check_dependency_freshness.sh --all --strict # CI
|
||||
#
|
||||
# Exit: 0 current | 1 behind | 2 allowlist wrong | 3 undeterminable (offline)
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_dependency_freshness.sh"
|
||||
|
||||
main() {
|
||||
if [[ ! -x "$SHARED_GATE" ]]; then
|
||||
echo "Error: shared dependency-freshness gate not found at $SHARED_GATE" >&2
|
||||
echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2
|
||||
echo " UTILS_ROOT to where it lives." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bash "$SHARED_GATE" "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
20
dep_freshness/templates/precommit-hook.yaml
Normal file
20
dep_freshness/templates/precommit-hook.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
# Every dependency on its ecosystem's newest STABLE release, exact-pinned
|
||||
# so the question is mechanically answerable at all.
|
||||
#
|
||||
# `files:` is what keeps this cheap: a commit touching only source makes
|
||||
# ZERO network requests, and manifest commits mostly hit a 6h disk cache.
|
||||
#
|
||||
# Runs WITHOUT --strict on purpose. Offline it degrades to the cached
|
||||
# answer and passes with a loud banner; `--no-verify` is banned here, so
|
||||
# a hook that hard-failed on a flaky connection would leave no way to
|
||||
# commit at all. CI, always online, is the strict authority.
|
||||
- id: dependency-freshness
|
||||
name: dependencies on newest stable
|
||||
entry: scripts/check_dependency_freshness.sh
|
||||
language: system
|
||||
pass_filenames: true
|
||||
files: >-
|
||||
(pubspec\.(yaml|lock)|pyproject\.toml|requirements.*\.txt|uv\.lock
|
||||
|package\.json|package-lock\.json|pnpm-lock\.yaml
|
||||
|Cargo\.(toml|lock)|go\.(mod|sum)
|
||||
|\.fvmrc|\.nvmrc|\.python-version)$
|
||||
57
dep_freshness/templates/workflow.yml
Normal file
57
dep_freshness/templates/workflow.yml
Normal file
@ -0,0 +1,57 @@
|
||||
name: dependency freshness
|
||||
|
||||
# Every dependency on its ecosystem's newest STABLE release, enforced on push
|
||||
# as well as at commit time. The local pre-commit hook only sees the manifests
|
||||
# in a commit and can be skipped entirely by a fresh clone that never ran
|
||||
# `pre-commit install`; this runs `--all` over the whole repo.
|
||||
#
|
||||
# `--strict` is CI-only. Offline the gate degrades to its cached answer and
|
||||
# passes, because `--no-verify` is banned locally and a hook that hard-failed
|
||||
# on a flaky connection would leave no way to commit. A runner is always
|
||||
# online, so here an undeterminable answer is a real failure.
|
||||
#
|
||||
# Expect this to go red for reasons this repo's commit did not cause: an
|
||||
# upstream release overnight is enough. That is the chosen behaviour -- the
|
||||
# alternative is a gate that only notices staleness when someone remembers to
|
||||
# look.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dependency-freshness:
|
||||
name: dependencies on newest stable
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# The gate lives in the shared utils repo so every repo resolves the
|
||||
# same "latest stable" the same way. Checking it out beats vendoring a
|
||||
# copy, which is exactly how a gate and its survey start disagreeing.
|
||||
- name: Check out the shared gate
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: kuhyx/utils
|
||||
path: .utils
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install the gate's two runtime dependencies
|
||||
run: python -m pip install --upgrade pyyaml packaging
|
||||
|
||||
- name: Cache registry lookups
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/dep-freshness
|
||||
key: dep-freshness-${{ hashFiles('**/pubspec.yaml', '**/pyproject.toml',
|
||||
'**/requirements*.txt', '**/package.json', '**/Cargo.toml', '**/go.mod') }}
|
||||
restore-keys: dep-freshness-
|
||||
|
||||
- name: Every dependency is on newest stable
|
||||
run: bash .utils/scripts/check_dependency_freshness.sh --all --strict
|
||||
182
dep_freshness/tests/test_install.py
Normal file
182
dep_freshness/tests/test_install.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""The gate installer: three pieces in, idempotent, and no comments lost.
|
||||
|
||||
The point of this module is that sixteen repos get byte-identical copies, so
|
||||
the assertions compare against the templates rather than against a paraphrase
|
||||
of them -- a test that spelled the expected content out by hand would let the
|
||||
template and the installed file drift, which is the failure the installer
|
||||
exists to prevent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dep_freshness import install as installer
|
||||
from dep_freshness.install_cli import main
|
||||
from dep_freshness.tests.conftest import write
|
||||
|
||||
TEMPLATES = Path(installer.__file__).resolve().parent / "templates"
|
||||
|
||||
|
||||
def _template(name: str) -> str:
|
||||
return (TEMPLATES / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_a_bare_repo_gets_all_three_pieces(repo):
|
||||
assert installer.install(repo) == [
|
||||
"scripts/check_dependency_freshness.sh",
|
||||
".github/workflows/dependency-freshness.yml",
|
||||
".pre-commit-config.yaml (dependency-freshness hook)",
|
||||
]
|
||||
delegate = repo / "scripts/check_dependency_freshness.sh"
|
||||
assert delegate.read_text(encoding="utf-8") == _template("delegate.sh")
|
||||
assert delegate.stat().st_mode & 0o111, "the hook execs it; it must be +x"
|
||||
assert (repo / ".github/workflows/dependency-freshness.yml").read_text(
|
||||
encoding="utf-8"
|
||||
) == _template("workflow.yml")
|
||||
assert "id: dependency-freshness" in (repo / ".pre-commit-config.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_a_second_run_writes_nothing(repo):
|
||||
installer.install(repo)
|
||||
assert installer.install(repo) == []
|
||||
assert installer.plan(repo) == []
|
||||
|
||||
|
||||
def test_plan_names_what_is_missing_without_writing_it(repo):
|
||||
assert installer.plan(repo) == [
|
||||
"scripts/check_dependency_freshness.sh",
|
||||
".github/workflows/dependency-freshness.yml",
|
||||
".pre-commit-config.yaml (dependency-freshness hook)",
|
||||
]
|
||||
assert not (repo / "scripts").exists()
|
||||
|
||||
|
||||
def test_a_drifted_copy_is_rewritten(repo):
|
||||
installer.install(repo)
|
||||
delegate = repo / "scripts/check_dependency_freshness.sh"
|
||||
delegate.write_text("#!/bin/bash\necho lol\n", encoding="utf-8")
|
||||
workflow = repo / ".github/workflows/dependency-freshness.yml"
|
||||
workflow.write_text("name: something else\n", encoding="utf-8")
|
||||
assert installer.install(repo) == [
|
||||
"scripts/check_dependency_freshness.sh",
|
||||
".github/workflows/dependency-freshness.yml",
|
||||
]
|
||||
assert delegate.read_text(encoding="utf-8") == _template("delegate.sh")
|
||||
|
||||
|
||||
def test_an_existing_pre_commit_config_keeps_its_hooks_and_comments(repo):
|
||||
"""Appending, not re-emitting: a YAML round-trip drops every comment.
|
||||
|
||||
Each of these configs explains why its hooks exist, and that prose is the
|
||||
only record of it.
|
||||
"""
|
||||
write(
|
||||
repo,
|
||||
".pre-commit-config.yaml",
|
||||
"""\
|
||||
# The 250-line cap. A file that cannot be read in one piece forces re-reads.
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: file-length-cap
|
||||
name: file length <= 250 lines
|
||||
entry: bash scripts/check_file_length.sh
|
||||
language: system
|
||||
""",
|
||||
)
|
||||
installer.install(repo)
|
||||
body = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
|
||||
assert "# The 250-line cap." in body
|
||||
assert "id: file-length-cap" in body
|
||||
assert "id: dependency-freshness" in body
|
||||
|
||||
|
||||
def test_a_config_that_already_has_the_hook_is_left_alone(repo):
|
||||
write(
|
||||
repo,
|
||||
".pre-commit-config.yaml",
|
||||
"""\
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: dependency-freshness
|
||||
entry: scripts/check_dependency_freshness.sh
|
||||
language: system
|
||||
""",
|
||||
)
|
||||
before = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
|
||||
installer.install(repo)
|
||||
assert (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_a_config_without_a_trailing_newline_still_appends_cleanly(repo):
|
||||
write(repo, ".pre-commit-config.yaml", "repos:\n - repo: local\n hooks:")
|
||||
installer.install(repo)
|
||||
body = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
|
||||
assert body.startswith("repos:\n - repo: local\n hooks:\n\n #")
|
||||
assert "id: dependency-freshness" in body
|
||||
|
||||
|
||||
def test_fvmrc_is_only_for_repos_that_hold_a_pubspec(repo):
|
||||
assert installer.write_fvmrc(repo, "3.47.2") is False
|
||||
assert not (repo / ".fvmrc").exists()
|
||||
write(repo, "app/pubspec.yaml", "name: demo\n")
|
||||
assert installer.write_fvmrc(repo, "3.47.2") is True
|
||||
assert (repo / ".fvmrc").read_text(
|
||||
encoding="utf-8"
|
||||
) == '{\n "flutter": "3.47.2"\n}\n'
|
||||
assert installer.write_fvmrc(repo, "3.47.2") is False
|
||||
|
||||
|
||||
def test_the_cli_installs_and_reports_each_piece(repo, capsys):
|
||||
assert main([str(repo)]) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "wrote: scripts/check_dependency_freshness.sh" in out
|
||||
assert "wrote: .github/workflows/dependency-freshness.yml" in out
|
||||
|
||||
|
||||
def test_the_cli_check_mode_writes_nothing(repo, capsys):
|
||||
assert main([str(repo), "--check"]) == 0
|
||||
assert (
|
||||
"would write: scripts/check_dependency_freshness.sh" in capsys.readouterr().out
|
||||
)
|
||||
assert not (repo / "scripts").exists()
|
||||
main([str(repo)])
|
||||
assert main([str(repo), "--check"]) == 0
|
||||
assert "gate already current" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_the_cli_pins_the_sdk_when_asked(repo, monkeypatch, capsys):
|
||||
monkeypatch.setattr(
|
||||
"dep_freshness.install_cli.flutter_latest", lambda: ("3.47.2", "3.13.2")
|
||||
)
|
||||
write(repo, "pubspec.yaml", "name: demo\n")
|
||||
assert main([str(repo), "--fvm"]) == 0
|
||||
assert "wrote: .fvmrc (flutter 3.47.2)" in capsys.readouterr().out
|
||||
# Same version again: the pin is already right, so nothing is reported.
|
||||
assert main([str(repo), "--fvm"]) == 0
|
||||
assert ".fvmrc" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_an_unresolvable_sdk_leaves_the_pin_alone(repo, monkeypatch, capsys):
|
||||
"""Offline, the alternative is pinning every repo to a guessed number."""
|
||||
monkeypatch.setattr(
|
||||
"dep_freshness.install_cli.flutter_latest", lambda: (None, None)
|
||||
)
|
||||
write(repo, "pubspec.yaml", "name: demo\n")
|
||||
assert main([str(repo), "--fvm"]) == 1
|
||||
assert "could not resolve latest stable Flutter" in capsys.readouterr().err
|
||||
assert not (repo / ".fvmrc").exists()
|
||||
|
||||
|
||||
def test_the_module_entrypoint_runs_the_cli(repo, monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["install_main", str(repo), "--check"])
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
runpy.run_module("dep_freshness.install_main", run_name="__main__")
|
||||
assert exit_info.value.code == 0
|
||||
85
scripts/install_dependency_freshness_gate.sh
Executable file
85
scripts/install_dependency_freshness_gate.sh
Executable file
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Install the dependency-freshness gate's three pieces into a repo.
|
||||
#
|
||||
# The gate is a delegate script + a pre-commit hook entry + a CI workflow, and
|
||||
# every repo needs all three. Copying them by hand across ~16 repos is what
|
||||
# produced four different ids for the 250-line cap, two of them independent
|
||||
# reimplementations, so the copy lives here and the templates have one home
|
||||
# (utils/dep_freshness/templates/).
|
||||
#
|
||||
# Idempotent: a second run reports "already current" and writes nothing.
|
||||
#
|
||||
# NOTE: never install the gate ahead of the upgrade. A repo that gets the gate
|
||||
# while its dependencies are still stale is instantly and permanently red --
|
||||
# gate and upgrade belong in the same commit.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/install_dependency_freshness_gate.sh <repo> [--check] [--fvm]
|
||||
#
|
||||
# Options:
|
||||
# --check report what a run would write, change nothing
|
||||
# --fvm also pin the Flutter SDK in <repo>/.fvmrc (Dart repos only)
|
||||
#
|
||||
# Exit: 0 installed or already current | 1 bad usage / missing repo
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly UTILS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
REPO=""
|
||||
CHECK=""
|
||||
FVM=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $SCRIPT_NAME <repo> [--check] [--fvm]"
|
||||
echo " --check report what would be written, change nothing"
|
||||
echo " --fvm also write <repo>/.fvmrc pinning the Flutter SDK"
|
||||
exit 0
|
||||
}
|
||||
|
||||
validate_requirements() {
|
||||
if [[ -z "$REPO" ]]; then
|
||||
echo "Error: a repository path is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$REPO" ]]; then
|
||||
echo "Error: no such directory: $REPO" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
validate_requirements
|
||||
PYTHONPATH="$UTILS_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
||||
python3 -m dep_freshness.install_main "$REPO" $CHECK $FVM
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--check)
|
||||
CHECK="--check"
|
||||
shift
|
||||
;;
|
||||
--fvm)
|
||||
FVM="--fvm"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
REPO="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
main
|
||||
Loading…
Reference in New Issue
Block a user