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
14 changes: 13 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ ARG BUILDIFIER_VERSION=v8.5.1
# uv: the workspace's Python package manager. Single source of truth for the
# uv.lock + requirements_lock.txt chain consumed by rules_python's pip.parse
# and enforced by the `uv-lock-fresh` pre-commit hook. Installed first so
# later PRs can layer `uv tool install ruff ty pre-commit` on top of it.
# later layers can layer `uv tool install ruff ty pre-commit` on top of it.
# Pinned tag bumped by Renovate's docker manager (matches the pattern used in
# ~/.dotfiles/.devcontainer/Dockerfile).
COPY --from=ghcr.io/astral-sh/uv:0.11.21 /uv /uvx /usr/local/bin/

# ruff: format + lint. Installed via `uv tool install` so the binary lives in
# /usr/local/bin and `ruff` is on PATH for the pre-commit hooks and the VS Code
# extension. UV_TOOL_DIR is forced to a world-readable system location because
# the install runs as root; the default ~/.local/share/uv would land under
# /root (mode 700) and the vscode user could not follow the bin/ symlink.
# Renovate's regex manager tracks RUFF_VERSION via the comment above the ARG.
# renovate: datasource=pypi depName=ruff
ARG RUFF_VERSION=0.15.17
ENV UV_TOOL_BIN_DIR=/usr/local/bin \
UV_TOOL_DIR=/usr/local/share/uv-tools
RUN uv tool install --no-cache "ruff==${RUFF_VERSION}"

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"vscode": {
"extensions": [
"bazelbuild.vscode-bazel",
"charliermarsh.ruff",
"cnshenj.vscode-task-manager",
"esbenp.prettier-vscode",
"github.vscode-github-actions",
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,56 @@ jobs:
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
- run: bazel run --config=ci //:gazelle -- -mode=diff

# The repo's `meta/scripts/*.py` target Python 3.14 (matches the rules_python
# toolchain pin in MODULE.bazel and the `target-version` in pyproject.toml's
# [tool.ruff]). Ubuntu-24.04 still ships 3.12 as `python3`, so the language-
# toolchain pin needs to be applied explicitly on every job that runs a script
# outside Bazel. The `# renovate:` comment above each `python-version` is
# picked up by the regex matcher in renovate.json and grouped with the
# MODULE.bazel python_version under "Language toolchain SDKs".

go-modules-check:
name: Go module completeness check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_go_modules.py

go-work-check:
name: go.work consistency/completeness check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_go_work.py

secrets-check:
name: Secrets check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_secrets_dir.py

no-cgo-check:
name: No-cgo policy check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
# `go list -deps` is what surfaces transitive cgo, so the runner needs Go in PATH.
# Use go.work as the source-of-truth — it's the workspace-level Go version directive
# and matches the version Bazel installs in MODULE.bazel (per-module go.mod files
Expand Down Expand Up @@ -79,6 +103,21 @@ jobs:
with:
working-directory: ${{ matrix.module }}

# Python's sibling of golangci-lint. Format + lint in one job; config lives in
# `[tool.ruff]` in //:pyproject.toml. Renovate's regex manager tracks the
# version pin via the comment above (matches the Dockerfile pattern).
ruff:
name: ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
with:
# renovate: datasource=pypi depName=ruff
version: "0.15.17"
args: format --check
- run: ruff check

# Build and test runs once per supported target platform, on a runner whose host matches
# the target. (Running tests natively per platform is the only way (without an emulation
# layer we do not have) to actually exercise platform-specific code paths and catch regressions
Expand All @@ -97,6 +136,7 @@ jobs:
secrets-check,
no-cgo-check,
golangci-lint,
ruff,
]
strategy:
fail-fast: false
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_go_modules.py

# ── Static Application Security Testing (SAST) ───────────────────────────────
Expand Down
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ repos:
pass_filenames: false
files: ^(pyproject\.toml|uv\.lock|requirements_lock\.txt)$

# ruff check first (with --fix), then ruff format — check's import
# sorting can produce output the formatter wants to retouch. Both fix in
# place; pre-commit re-flags modified files so the user re-stages.
- id: ruff-check
name: ruff check --fix
language: system
entry: ruff check --fix
pass_filenames: false
files: \.py$

- id: ruff-format
name: ruff format
language: system
entry: ruff format
pass_filenames: false
files: \.py$

- id: gazelle
name: gazelle
language: system
Expand Down
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"recommendations": [
"bazelbuild.vscode-bazel",
"charliermarsh.ruff",
"esbenp.prettier-vscode",
"github.vscode-github-actions",
"golang.go",
Expand Down
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"prettier.proseWrap": "always",
"prettier.trailingComma": "all",
"python.languageServer": "Default",
"// Ruff format-on-save": "charliermarsh.ruff surfaces diagnostics inline; this block makes it the default Python formatter so `editor.formatOnSave` calls ruff format, and runs ruff check --fix + import-organize as save-time code actions. Config is read from `[tool.ruff]` in //:pyproject.toml — single source of truth shared with CI + pre-commit.",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"// Task Manager favorites": "Pins the Bazel build/test tasks to the cnshenj.vscode-task-manager sidebar (recommended in the devcontainer; see devcontainer.json). Names must match the `label` fields in tasks.json. The `UnnaturalDesigns/Workspace/` prefix is Task Manager's own scoping convention.",
"taskManager.favorites": [
"UnnaturalDesigns/Workspace/Bazel Build all",
Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Two GitHub Actions workflows run on every push and pull request to `main`.
| Secrets check | Always - verifies the `secrets/` directory contains no committed files |
| No-cgo policy check | Always - rejects `import "C"` and transitive deps that compile C/C++/cgo/SWIG |
| golangci-lint | After module check passes - runs per Go module |
| ruff | Always - `ruff format --check` and `ruff check` over all Python |
| Build and test | After all checks above pass |
| Coverage | After build and test - `bazel coverage //...`, uploads merged lcov to Codecov |

Expand All @@ -181,23 +182,29 @@ each commit. To install:
pre-commit install
```

Only hooks that either fix the problem they detect (`bazel-mod-tidy`, `gazelle`) or prevent unsafe
content from entering the repo (`check-secrets-dir`) run here. Verification-only checks live in the
editor instead (see **Editor integration** below) so they can surface findings without blocking a
commit when you want to switch contexts.
Only hooks that either fix the problem they detect (`bazel-mod-tidy`, `gazelle`, `uv-lock-fresh`,
`ruff-check`, `ruff-format`) or prevent unsafe content from entering the repo (`check-secrets-dir`)
run here. Verification-only checks live in the editor instead (see **Editor integration** below) so
they can surface findings without blocking a commit when you want to switch contexts.

| Hook | Triggers on |
| ------------------- | ----------------------------- |
| `bazel-mod-tidy` | `go.mod`, `go.work`, `go.sum` |
| `gazelle` | `*.go` files |
| `check-secrets-dir` | files under `secrets/` |
| Hook | Triggers on |
| ------------------- | -------------------------------------------- |
| `bazel-mod-tidy` | `go.mod`, `go.work`, `go.sum` |
| `uv-lock-fresh` | `pyproject.toml`, `uv.lock`, `requirements_lock.txt` |
| `ruff-check` | `*.py` files |
| `ruff-format` | `*.py` files |
| `gazelle` | `*.go` files |
| `check-secrets-dir` | files under `secrets/` |

**Editor integration** (via `.vscode/`) - runs the non-fixing checks on save. Works in VS Code and
VS Code-derived editors (e.g. Google Antigravity). Recommended extensions
(`.vscode/extensions.json`):

- [`golang.go`](https://marketplace.visualstudio.com/items?itemName=golang.go) - runs
`golangci-lint` on save at package scope, surfacing inline findings that match what CI enforces.
- [`charliermarsh.ruff`](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) -
surfaces `ruff check` diagnostics inline and applies `ruff format` on save, matching what the CI
`ruff` job and the pre-commit hooks enforce.
- [`emeraldwalk.runonsave`](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) -
triggers the repo-health scripts on save.
- [`ryanluker.vscode-coverage-gutters`](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) -
Expand All @@ -206,6 +213,7 @@ VS Code-derived editors (e.g. Google Antigravity). Recommended extensions
| On-save check | Triggers on |
| -------------------- | ------------------------------------------ |
| `golangci-lint` | `*.go` files |
| `ruff` (diagnostics + format) | `*.py` files |
| `check-go-modules` | `go.mod`, workflow `.yml`, `.golangci.yml` |
| `check-go-work` | `go.mod`, `go.work` |

Expand Down
9 changes: 5 additions & 4 deletions meta/scripts/_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
def workspace_root() -> Path:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, check=True,
capture_output=True,
text=True,
check=True,
)
return Path(result.stdout.strip())

Expand All @@ -27,8 +29,7 @@ def is_skipped(path: Path) -> bool:
that should never contain repo-managed sources.
"""
return any(
part in _SKIP_DIR_NAMES or part.startswith(_SKIP_DIR_PREFIXES)
for part in path.parts
part in _SKIP_DIR_NAMES or part.startswith(_SKIP_DIR_PREFIXES) for part in path.parts
)


Expand All @@ -54,7 +55,7 @@ def col_range(file: Path, lineno: int, needle: str) -> tuple[int, int]:
line = file.read_text().splitlines()[lineno - 1]
start = line.index(needle) + 1
return start, start + len(needle)
except (OSError, IndexError, ValueError):
except OSError, IndexError, ValueError:
# bad path / past-EOF lineno / needle-not-on-line all collapse to the same fallback.
return 1, 2

Expand Down
19 changes: 12 additions & 7 deletions meta/scripts/check_go_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# where rules_python already makes the import resolvable.
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from meta.scripts._workspace import col_range, found_modules, workspace_root # noqa: E402
from meta.scripts._workspace import col_range, found_modules, workspace_root


def workflow_module_lists(
Expand Down Expand Up @@ -52,7 +52,7 @@ def workflow_module_lists(
text = workflow_file.read_text()
result: list[tuple[str, int, dict[Path, int]]] = []

state = "scanning" # scanning | in_matrix | in_module
state = "scanning" # scanning | in_matrix | in_module
matrix_indent = -1
module_indent = -1
module_key_line = -1
Expand Down Expand Up @@ -109,9 +109,8 @@ def workflow_module_lists(
module_key_line = lineno
current = {}

elif state == "in_module":
if stripped.startswith("- "):
current[Path(stripped[2:].strip())] = lineno
elif state == "in_module" and stripped.startswith("- "):
current[Path(stripped[2:].strip())] = lineno

# End of file while still inside a module list.
if state == "in_module" and current is not None:
Expand Down Expand Up @@ -146,7 +145,10 @@ def check_workflow_matrices(root: Path, modules: set[Path]) -> int:
for mod in sorted(matrix_set - modules):
line = matrix_entries[mod]
start, end = col_range(wf_file, line, str(mod))
print(f"{rel}:{line}:{start}-{end}: [{job_name}] stale matrix entry ./{mod} (no go.mod)")
print(
f"{rel}:{line}:{start}-{end}: "
f"[{job_name}] stale matrix entry ./{mod} (no go.mod)"
)
errors += 1

return errors
Expand Down Expand Up @@ -174,7 +176,10 @@ def check_golangci_configs(root: Path, modules: set[Path]) -> int:
candidate = candidate.parent
if not found:
# Anchor on the module's go.mod — no specific token at fault.
print(f"{mod}/go.mod:1:1-2: no .golangci.yml reachable from ./{mod} (module dir or any parent up to repo root)")
print(
f"{mod}/go.mod:1:1-2: no .golangci.yml reachable from ./{mod} "
f"(module dir or any parent up to repo root)"
)
errors += 1
return errors

Expand Down
2 changes: 1 addition & 1 deletion meta/scripts/check_go_work.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# where rules_python already makes the import resolvable.
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from meta.scripts._workspace import ( # noqa: E402
from meta.scripts._workspace import (
col_range,
found_modules,
registered_modules,
Expand Down
16 changes: 6 additions & 10 deletions meta/scripts/check_no_cgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
# where rules_python already makes the import resolvable.
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from meta.scripts._workspace import ( # noqa: E402
from meta.scripts._workspace import (
find_files,
registered_modules,
workspace_root,
Expand All @@ -55,7 +55,7 @@ def find_cgo_in_sources(root: Path) -> list[Path]:
for go_file in find_files(root, "*.go"):
try:
content = go_file.read_text()
except (UnicodeDecodeError, OSError):
except UnicodeDecodeError, OSError:
continue
if _CGO_IMPORT_RE.search(content):
offenders.append(go_file.relative_to(root))
Expand Down Expand Up @@ -92,9 +92,7 @@ def find_cgo_in_deps(module_dir: Path) -> list[str]:
check=False,
)
if result.returncode != 0:
raise RuntimeError(
f"`go list` failed in {module_dir}:\n{result.stderr.strip()}"
)
raise RuntimeError(f"`go list` failed in {module_dir}:\n{result.stderr.strip()}")
return [line.strip() for line in result.stdout.splitlines() if line.strip()]


Expand All @@ -104,7 +102,7 @@ def check(root: Path) -> int:
sources = find_cgo_in_sources(root)
if sources:
exit_code = 1
print("Found `import \"C\"` in repo source files:")
print('Found `import "C"` in repo source files:')
for path in sources:
print(f" {path}")
print()
Expand All @@ -130,9 +128,7 @@ def check(root: Path) -> int:
continue
if offenders:
exit_code = 1
print(
f"Module //{module_rel} has dependencies that compile C/C++/cgo/SWIG:"
)
print(f"Module //{module_rel} has dependencies that compile C/C++/cgo/SWIG:")
for path in offenders:
print(f" {path}")
print()
Expand All @@ -143,7 +139,7 @@ def check(root: Path) -> int:
else:
print(
"Pure-Go policy violation. See docs/future-considerations.md "
"(\"Introducing cgo or Python C-Extensions\") for the implications "
'("Introducing cgo or Python C-Extensions") for the implications '
"and required infrastructure changes."
)

Expand Down
2 changes: 1 addition & 1 deletion meta/scripts/smoke_py/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class PyPiHubReachableTest(unittest.TestCase):
def test_third_party_import_resolves(self) -> None:
import requests # noqa: PLC0415 - import inside test is the point
import requests

self.assertTrue(hasattr(requests, "get"))

Expand Down
Loading
Loading