diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 6d2d1fa..d4905d6 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -89,6 +89,12 @@ that label the test fails until you look up the new image's `python3` (actions/runner-images) and update `RUNNER_IMAGE_PYTHON`, `RUNNER_PYTHON` and `ruff.toml` together. +- Formatting: base has no ruff dependency of its own, so `ruff-check`/`ruff-format` + from `astral-sh/ruff-pre-commit` in `../../.pre-commit-config.yaml`, scoped + to `^\.github/scripts/.*\.py$`, keep these scripts formatted to `ruff.toml`. + Unformatted scripts would otherwise fail every Python instance's own + `ruff format` hook after a sync. Keep that hook's `rev` equal to the ruff + the Python templates pin. - `test_moderate_labels.py` — checks that every `create "" "" ""` line in `../workflows/moderate-setup.yml` has a description of at most 100 characters (GitHub answers HTTP 422 to a longer one, and only diff --git a/.github/scripts/ruff.toml b/.github/scripts/ruff.toml index dc0a302..dfa88ee 100644 --- a/.github/scripts/ruff.toml +++ b/.github/scripts/ruff.toml @@ -23,6 +23,8 @@ line-length = 100 [lint] # Ruff's long-standing defaults, spelled out so a newer ruff that widens -# its default rule set does not start flagging these scripts. Base itself -# does not run ruff; this keeps `ruff check` quiet in every checkout. +# its default rule set does not start flagging these scripts. Base runs +# ruff on this directory only through the ruff-pre-commit hook in +# ../../.pre-commit-config.yaml; this keeps `ruff check` quiet in every +# checkout. select = ["E4", "E7", "E9", "F"] diff --git a/.github/scripts/test_moderate_apply_result.py b/.github/scripts/test_moderate_apply_result.py index 4e8b025..5b34b86 100644 --- a/.github/scripts/test_moderate_apply_result.py +++ b/.github/scripts/test_moderate_apply_result.py @@ -280,7 +280,9 @@ def test_base_sha_must_be_a_full_hex_sha(self) -> None: (self.tmp / "log.txt").unlink(missing_ok=True) proc = self.run_script(meta_extra={"base_sha": bad}) log = self.log() - self.assertFalse([c for c in log if c.startswith(("git fetch", "git am", "gh api"))]) + self.assertFalse( + [c for c in log if c.startswith(("git fetch", "git am", "gh api"))] + ) self.assert_failed_cleanly(proc, log) def test_denied_path_in_the_patch_is_never_pushed(self) -> None: @@ -317,7 +319,13 @@ def test_ordinary_paths_pass(self) -> None: """Source, tests and docs (also next to look-alike names) are accepted.""" diff = "".join( f":100644 100644 a b M\\0{p}\\0" - for p in ("src/a.py", "tests/test_a.py", "docs/Makefile.md", "scripts/other.sh", ".githubx/y") + for p in ( + "src/a.py", + "tests/test_a.py", + "docs/Makefile.md", + "scripts/other.sh", + ".githubx/y", + ) ) proc = self.run_script(run_env={"FAKE_DIFF": diff}) self.assertEqual(proc.returncode, 0, proc.stderr) diff --git a/.github/scripts/test_moderate_tools.py b/.github/scripts/test_moderate_tools.py index 6ed9244..c695e8d 100644 --- a/.github/scripts/test_moderate_tools.py +++ b/.github/scripts/test_moderate_tools.py @@ -270,7 +270,9 @@ def test_key_only_from_the_environment(self) -> None: self.assertIn("KEY:sk-test", lines) self.assertIn("SCRUB:1", lines) self.assertIn("AUTHOR:claude-moderator[bot]", lines) - self.assertEqual(lines[lines.index("ARG:--allowedTools") + 1], "ARG:Read,Grep,Bash(uv run pytest *)") + self.assertEqual( + lines[lines.index("ARG:--allowedTools") + 1], "ARG:Read,Grep,Bash(uv run pytest *)" + ) settings = json.loads(lines[lines.index("ARG:--settings") + 1].removeprefix("ARG:")) self.assertIn("Read(//proc/**)", settings["permissions"]["deny"]) sandbox = settings["sandbox"] @@ -281,14 +283,18 @@ def test_key_only_from_the_environment(self) -> None: self.assertNotIn("enableWeakerNestedSandbox", sandbox) self.assertEqual(sandbox["network"]["allowedDomains"], ["github.com", "api.github.com"]) self.assertIs(sandbox["network"]["strictAllowlist"], True) - self.assertIn({"name": "ANTHROPIC_API_KEY", "mode": "deny"}, sandbox["credentials"]["envVars"]) + self.assertIn( + {"name": "ANTHROPIC_API_KEY", "mode": "deny"}, sandbox["credentials"]["envVars"] + ) self.assertEqual(lines[lines.index("ARG:-p") + 1], "ARG:do the thing") def test_missing_or_bad_domains_fail_before_claude_runs(self) -> None: """No network allow-list, or a malformed host, means claude never starts.""" for domains in ("", ",", 'x.com","evil', "a b"): with self.subTest(domains=domains): - proc = self.launch({"ANTHROPIC_API_KEY": "k", "MODERATION_ALLOWED_DOMAINS": domains}) + proc = self.launch( + {"ANTHROPIC_API_KEY": "k", "MODERATION_ALLOWED_DOMAINS": domains} + ) self.assertEqual(proc.returncode, 1) self.assertFalse(self.out.exists()) diff --git a/.github/template-sync-state.json b/.github/template-sync-state.json index 51bc1b0..02ff3c6 100644 --- a/.github/template-sync-state.json +++ b/.github/template-sync-state.json @@ -1,4 +1,4 @@ { "template_repo": "craftainer/template-base", - "synced_tag": "v1.15.0" + "synced_tag": "v1.15.1" } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70cb725..13d2e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,9 @@ repos: - id: check-added-large-files stages: [pre-commit, manual] - id: check-merge-conflict + # Without this the hook only runs during an in-progress git merge, so + # conflict markers committed by a template-sync PR were never caught. + args: [--assume-in-merge] stages: [pre-commit, manual] - id: mixed-line-ending stages: [pre-commit, manual] @@ -48,6 +51,25 @@ repos: - id: conventional-pre-commit stages: [commit-msg] + # Guard for the upstream-owned scripts in .github/scripts/: base ships + # them to every instance, whose own ruff-format hook then checks them + # against .github/scripts/ruff.toml (the closest config wins), so an + # unformatted script here fails every Python instance after a sync. Base + # has no ruff dependency of its own, hence this pinned hook, scoped to + # that directory only. Keep the rev equal to the ruff the Python + # templates pin in pyproject.toml. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.8 + hooks: + - id: ruff-check + name: ruff check (.github/scripts) + files: ^\.github/scripts/.*\.py$ + stages: [pre-commit, manual] + - id: ruff-format + name: ruff format (.github/scripts) + files: ^\.github/scripts/.*\.py$ + stages: [pre-commit, manual] + # This instance's own hooks: the features' shell code. - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.11.0.1 diff --git a/docs/TEMPLATE.md b/docs/TEMPLATE.md index 542ca5e..a8b4620 100644 --- a/docs/TEMPLATE.md +++ b/docs/TEMPLATE.md @@ -103,13 +103,20 @@ instance's own toolchain needs. `.pre-commit-config.yaml` defines whitespace/EOF fixers, YAML/TOML/JSON checks, `conventional-pre-commit` (Conventional Commits, enforced at the `commit-msg` stage), the `template-sync-manifest` completeness check, and -the `devcontainer-lock` lockfile-consistency check, and three security -scanners (below). An instance layers +the `devcontainer-lock` lockfile-consistency check, `ruff-check`/`ruff-format` +(`astral-sh/ruff-pre-commit`, scoped to `.github/scripts/*.py` so base can't +ship scripts that fail a Python instance's own ruff hook; keep its `rev` equal +to the ruff the Python templates pin), and three security scanners (below). An instance layers its own language-specific hooks on top (lint, format, type check, test suite, dependency-vulnerability scan, ...), matching the shape `template-fastapi`'s own `.pre-commit-config.yaml` already establishes for `ruff`/`mypy`/`pytest`/`pip-audit`. +`check-merge-conflict` runs with `--assume-in-merge`, so it flags committed +conflict markers (a line starting `<<<<<<< `, `=======` or `>>>>>>> `) in any +file, not just during an in-progress git merge; a template sync PR that +commits a merge-tier conflict is then caught in CI. + ### Security scanners | Hook | Scans | Stage | Network | OWASP (2025) |