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
6 changes: 6 additions & 0 deletions .github/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<name>" "<color>"
"<description>"` line in `../workflows/moderate-setup.yml` has a description
of at most 100 characters (GitHub answers HTTP 422 to a longer one, and only
Expand Down
6 changes: 4 additions & 2 deletions .github/scripts/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
12 changes: 10 additions & 2 deletions .github/scripts/test_moderate_apply_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions .github/scripts/test_moderate_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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())

Expand Down
2 changes: 1 addition & 1 deletion .github/template-sync-state.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"template_repo": "craftainer/template-base",
"synced_tag": "v1.15.0"
"synced_tag": "v1.15.1"
}
22 changes: 22 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
11 changes: 9 additions & 2 deletions docs/TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Loading