Skip to content

ci(security): drop the modules-check job, it re-derives ci.yml's answer #1092

ci(security): drop the modules-check job, it re-derives ci.yml's answer

ci(security): drop the modules-check job, it re-derives ci.yml's answer #1092

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Supersede in-flight runs for the same PR; see .claude/CLAUDE.md "Superseding CI runs".
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
gazelle-check:
name: Gazelle BUILD file check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-bazel-remote
with:
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
- run: bazel run --config=ci //:gazelle -- -mode=diff
# MODULE.bazel.lock is the one derived file with no CI backstop of its own: builds run with
# `--lockfile_mode=update`, which rewrites the lock in memory and stays green, so a `bazel mod
# tidy` hook that never ran is invisible until it blocks someone's commit. Fail-only on
# purpose — the regenerated lock belongs in the authoring commit, not in a CI push.
#
# `bazel mod tidy` also rewrites MODULE.bazel's `use_repo` lines, so both files are diffed.
bazel-lock-check:
name: MODULE.bazel.lock freshness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-bazel-remote
with:
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
- run: |
set -euo pipefail
# `--config=ci` for consistency with gazelle-check next door. It changes nothing
# here — `mod tidy` executes no actions, so BES and remote execution are moot.
bazel mod tidy --config=ci
if ! git diff --exit-code -- MODULE.bazel MODULE.bazel.lock; then
MSG="stale — run 'bazel mod tidy' and commit the result."
echo "::error file=MODULE.bazel.lock::${MSG}"
exit 1
fi
echo "MODULE.bazel and MODULE.bazel.lock are what 'bazel mod tidy' produces."
# 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".
# Polyglot module/project completeness check. Per language (Go, Python):
# config-file reachability + workflow matrix consistency. Python-only:
# root pyproject.toml has the required tool sections, every per-project
# pyproject is covered by [tool.uv.workspace].members, and
# requirements_lock.txt is fresh relative to uv.lock. uv is needed for the
# last check — setup-uv brings both uv and a 3.14 Python interpreter.
modules-check:
name: Module completeness check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_modules.py
go-work-check:
name: go.work consistency/completeness check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_go_work.py
adr-numbers-check:
name: ADR number uniqueness check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
# renovate: datasource=python-version depName=python
python-version: "3.14"
- run: python3 meta/scripts/check_adr_numbers.py
secrets-check:
name: Secrets check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
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
# only declare each module's minimum, which can drift below the workspace version).
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.work
# This step only runs `go list -deps` via check_no_cgo.py — no `go build`
# of our code, no `go mod download`. With cache enabled (the default),
# setup-go searches for go.mod/go.sum at the repo root to build a cache
# key and warns when it can't find them (this is a Bazel monorepo with
# per-module go.mod files, none at the root).
cache: false
- run: python3 meta/scripts/check_no_cgo.py
# TEND(lang-expand): this job lints Go only. When a new language is adopted, add a sibling
# job (e.g. ruff for Python) — do not extend this matrix, the linter is Go-specific.
golangci-lint:
name: golangci-lint (${{ matrix.go_module }})
runs-on: ubuntu-latest
needs: [modules-check]
strategy:
fail-fast: false
matrix:
go_module:
- tools/network_infrastructure_maintenance
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
working-directory: ${{ matrix.go_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).
# TEND(project-expand): repo-wide today because there is no per-project
# tuning. When real Python projects land and want per-project ruff config,
# convert this to a matrix-per-project job (mirroring golangci-lint) and
# add a matching matrix-completeness callsite to check_modules.py keyed off
# a Python matrix key (matrix.module: is currently Go-only).
ruff:
name: ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
with:
# renovate: datasource=pypi depName=ruff
version: "0.16.4"
args: format --check
- run: ruff check
# shellcheck: the enforcing half. The editor surfaces the same findings inline on save
# (timonwong.shellcheck, configured in .vscode/settings.json), but nothing blocks a commit on
# them, so this job is where shell lint is actually gated. Scoped to *.sh, which today means
# .devcontainer/'s host stub and lifecycle hooks plus meta/devcontainer-base/scripts/ —
# privileged writes against /etc and host-absolute paths, where `set -e` foot-guns (errexit
# suspension inside `||` callees, most recently) are worth catching mechanically rather than
# in review.
# The version is the devcontainer's, read out of its Dockerfile rather than restated here —
# the runner image ships its own shellcheck, and using it would make the gating job and the
# editor separately versioned, with a finding's presence depending on where you looked.
shellcheck:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install the pinned shellcheck
run: |
set -euo pipefail
version="$(sed -n 's/^ARG SHELLCHECK_VERSION=//p' .devcontainer/Dockerfile)"
# An ARG that moved or got renamed must fail here, not silently fall through to
# whatever the runner image happens to ship.
if [ -z "${version}" ]; then
echo "no SHELLCHECK_VERSION in .devcontainer/Dockerfile" >&2
exit 1
fi
curl -fsSL \
"https://github.com/koalaman/shellcheck/releases/download/${version}/shellcheck-${version}.linux.x86_64.tar.xz" \
| tar -xJ -C /tmp
# Ahead of the runner's own copy on PATH.
sudo install -m 0755 "/tmp/shellcheck-${version}/shellcheck" /usr/local/bin/shellcheck
# That ordering is an assumption about the runner image, and the whole point of the
# pin is that the gate and the editor cannot disagree — so a shadowed binary has to
# fail here rather than lint green under some other version.
on_path="$(shellcheck --version | sed -n 's/^version: //p')"
if [ "${on_path}" != "${version#v}" ]; then
echo "shellcheck on PATH is ${on_path}, expected ${version#v}" >&2
exit 1
fi
- run: |
set -euo pipefail
shellcheck --version | sed -n '2p'
mapfile -t files < <(git ls-files '*.sh')
# shellcheck itself exits 1 on an empty argument list; can't happen today, but a
# future .sh-free tree should report "nothing to check", not a red job.
if [ "${#files[@]}" -eq 0 ]; then
echo "no shell scripts tracked; nothing to check"
exit 0
fi
printf 'checking %d file(s)\n' "${#files[@]}"
shellcheck "${files[@]}"
# ty: Astral's static type checker. Config lives in `[tool.ty]` in
# //:pyproject.toml — same single-source-of-truth posture as ruff. No
# dedicated GitHub Action exists yet (ty is still alpha), so we install uv
# and run `uvx ty@<pin> check`. The pin is Renovate-tracked via the comment
# above TY_VERSION.
# TEND(project-expand): see the ruff job's TEND — same reasoning. When
# per-project ty config is needed, convert to a matrix-per-project job and
# extend check_modules.py with the matching matrix-completeness callsite.
ty:
name: ty
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
# `uv sync` materializes `.venv` from uv.lock so ty can resolve
# third-party imports (e.g. the smoke target's `requests`). Without it,
# ty's only search paths are first-party + stdlib and any non-stdlib
# import fails with `unresolved-import`.
- run: uv sync
- name: ty check
env:
# renovate: datasource=pypi depName=ty
TY_VERSION: "0.0.74"
run: uvx "ty@${TY_VERSION}" 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
# early.) A failure on any matrix entry blocks the merge because build-and-test-all is a
# required status check.
#
# If a future supported target lacks a matching GitHub-hosted runner, that entry would
# build-only (drop `test`); none of our current targets are in that situation.
build-and-test-per-target:
name: Build and test (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: [
gazelle-check,
modules-check,
go-work-check,
secrets-check,
no-cgo-check,
golangci-lint,
ruff,
ty,
]
strategy:
fail-fast: false
matrix:
include:
- platform: linux_x86_64
runner: ubuntu-latest
- platform: linux_arm64
runner: ubuntu-24.04-arm
- platform: darwin_arm64
runner: macos-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-bazel-remote
with:
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
# `--config=ci` brings in remote cache + BES; `--config=<platform>` pins the target
# platform explicitly. Each row's runner matches its target arch — required for
# Python (`@pypi` wheel resolution is host-bound; see
# docs/adr/0001-go-builds-are-pure-python-is-not.md). The matching runner also keeps
# test execution native: BB
# executors of the same arch are registered as candidates via `--config=<platform>`,
# darwin executes on the runner itself since BB has no macOS executors.
- run: bazel test --config=ci --config=${{ matrix.platform }} //...
build-and-test-all:
name: Build and test (all targets)
runs-on: ubuntu-latest
needs: [build-and-test-per-target]
if: always()
steps:
- name: Verify all build-and-test-per-target jobs passed
run: |
if [ "${{ needs.build-and-test-per-target.result }}" != "success" ]; then
echo "build-and-test-all result: ${{ needs.build-and-test-per-target.result }}"
exit 1
fi
echo "All build-and-test-per-target jobs passed"
coverage:
name: Coverage
runs-on: ubuntu-latest
needs: [build-and-test-all]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-bazel-remote
with:
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
# Coverage-specific flags (--combined_report, --experimental_fetch_all_coverage_outputs,
# --strategy=CoverageReport=local under :remote_bb) live in .bazelrc under the `coverage`
# command scope, so `bazel coverage //...` produces an identical report locally and in CI.
#
# Coverage runs on a single platform (linux_x86_64) by deliberate choice: it measures
# source-line coverage, which is platform-independent for pure-Go code without build
# tags. If we ever introduce platform-conditional code that needs per-target coverage
# measurement, this job would become a matrix like build-and-test.
#
# TEND(lang-expand): `bazel coverage //...` picks up every language whose rules emit
# coverage (rules_go does automatically; rules_python is wired via
# `configure_coverage_tool = True` on `python.toolchain` in MODULE.bazel). When adopting
# a new language, confirm its ruleset is wired into Bazel's coverage collection — e.g.
# rules_java needs JaCoCo. If coverage output is silently missing for a language,
# that's the thing to check.
- run: bazel coverage --config=ci --config=linux_x86_64 //...
- name: Locate merged lcov
id: lcov
run: echo "path=$(bazel info output_path)/_coverage/_coverage_report.dat" >> "$GITHUB_OUTPUT"
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ${{ steps.lcov.outputs.path }}
disable_search: true
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}