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
49 changes: 49 additions & 0 deletions .github/actions/go-setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: "Go setup (cached, Windows-tuned)"
description: >-
Set up Go with module + build caching and, on Windows, Microsoft Defender
exclusions for the Go caches and workspace. Windows `go test`/`go build` is
dominated by Defender real-time scanning of the many small object files the Go
toolchain writes during compile/link; excluding the cache dirs, the workspace,
and the go process is the single biggest wall-clock lever. This action is the
repo-wide standard entrypoint for Go jobs, enforced by the CI speed policy
lint (.github/scripts/lint_go_ci_policy.py).
inputs:
go-version-file:
description: "Path to the go.mod that pins the toolchain version."
required: true
cache-dependency-path:
description: >-
Path to go.sum (preferred) or go.mod used to key the module/build cache.
required: false
default: ""
runs:
using: composite
steps:
# Runs BEFORE setup-go so the cache tarball is extracted into already-excluded
# directories. Uses the fixed Windows runner defaults (GOCACHE =
# %LOCALAPPDATA%\go-build, GOPATH/GOMODCACHE under %USERPROFILE%\go) rather
# than `go env`, which is unavailable until the toolchain is installed.
- name: Exclude Go caches from Microsoft Defender (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$targets = @(
"$env:LOCALAPPDATA\go-build", # GOCACHE (build cache)
"$env:USERPROFILE\go", # GOPATH incl. pkg\mod (GOMODCACHE)
$env:GITHUB_WORKSPACE, # sources + compiled test binaries
$env:RUNNER_TEMP
) | Where-Object { $_ -and $_.Trim() -ne '' } | Select-Object -Unique
foreach ($t in $targets) {
try {
Add-MpPreference -ExclusionPath $t -ErrorAction Stop
Write-Host "Defender exclusion added: $t"
} catch {
Write-Host "::warning::Defender exclusion failed for $t : $($_.Exception.Message)"
}
}
try { Add-MpPreference -ExclusionProcess 'go.exe' -ErrorAction Stop } catch {}
- name: Set up Go (with module + build cache)
uses: actions/setup-go@v7
with:
go-version-file: ${{ inputs.go-version-file }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
132 changes: 132 additions & 0 deletions .github/scripts/lint_go_ci_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Repo-wide CI speed policy: every job that compiles Go must go through the
shared cached/Windows-tuned setup action.

Motivation: Windows `go test`/`go build` is dominated by Microsoft Defender
scanning the many small files the Go toolchain emits. The fix (Defender
exclusions + module/build cache) lives in `.github/actions/go-setup`. This lint
makes adoption structural instead of something a contributor has to remember:
any job whose `run:` steps invoke `go test|build|run|vet|install` must contain a
step that `uses:` the shared action. CI fails otherwise.

Escape hatch: put a line comment `# ci-speed-policy: exempt(<reason>)` anywhere
in a workflow file to skip that whole file (used for jobs that legitimately
cannot use the shared action). The reason is required so waivers are visible in
review.

Usage: python .github/scripts/lint_go_ci_policy.py [--workflows-dir DIR]
Exit code 0 = compliant, 1 = violations.
"""
from __future__ import annotations

import argparse
import glob
import os
import re
import sys

import yaml

# `go test`/`go build`/... as a standalone command. Word boundary before `go`
# keeps `cargo build` / `go.mod` / `go-version` from matching.
GO_CMD = re.compile(r"\bgo\s+(?:test|build|run|vet|install)\b")
SHARED_ACTION = "./.github/actions/go-setup"
EXEMPT = re.compile(r"#\s*ci-speed-policy:\s*exempt\(([^)]*)\)")


def _iter_run_and_uses(steps):
"""Yield ('run', text) and ('uses', ref) for each step in a job."""
if not isinstance(steps, list):
return
for step in steps:
if not isinstance(step, dict):
continue
run = step.get("run")
if isinstance(run, str):
yield "run", run
uses = step.get("uses")
if isinstance(uses, str):
yield "uses", uses


def check_workflow(path: str) -> list[str]:
with open(path, "r", encoding="utf-8") as fh:
raw = fh.read()

m = EXEMPT.search(raw)
if m:
reason = m.group(1).strip()
if not reason:
return [f"{path}: ci-speed-policy exempt marker requires a reason"]
return []

try:
doc = yaml.safe_load(raw)
except yaml.YAMLError as exc: # pragma: no cover - surfaced as a violation
return [f"{path}: could not parse YAML ({exc})"]

if not isinstance(doc, dict):
return []

jobs = doc.get("jobs")
if not isinstance(jobs, dict):
return []

problems: list[str] = []
for job_name, job in jobs.items():
if not isinstance(job, dict):
continue
runs_go = False
uses_shared = False
for kind, value in _iter_run_and_uses(job.get("steps")):
if kind == "run" and GO_CMD.search(value):
runs_go = True
elif kind == "uses" and value.strip().startswith(SHARED_ACTION):
uses_shared = True
if runs_go and not uses_shared:
problems.append(
f"{path}: job '{job_name}' runs a Go build/test but does not "
f"use '{SHARED_ACTION}' (add it, or exempt the file with a "
f"'# ci-speed-policy: exempt(<reason>)' comment)"
)
return problems


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--workflows-dir",
default=".github/workflows",
help="Directory of workflow YAML files to lint.",
)
args = parser.parse_args()

paths = sorted(
glob.glob(os.path.join(args.workflows_dir, "*.yml"))
+ glob.glob(os.path.join(args.workflows_dir, "*.yaml"))
)
if not paths:
print(f"No workflow files found under {args.workflows_dir}", file=sys.stderr)
return 1

violations: list[str] = []
for path in paths:
violations.extend(check_workflow(path))

if violations:
print("CI speed policy violations:\n")
for v in violations:
print(f" - {v}")
print(
"\nEvery job that compiles Go must use the shared "
f"'{SHARED_ACTION}' action so build caching and Windows Defender "
"exclusions are applied uniformly."
)
return 1

print(f"CI speed policy: OK ({len(paths)} workflow files checked)")
return 0


if __name__ == "__main__":
raise SystemExit(main())
8 changes: 4 additions & 4 deletions .github/workflows/boatstack-lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ jobs:
working-directory: labs/12-product-engineering-loop/product-engineering-loop
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/12-product-engineering-loop/product-engineering-loop/go.mod
cache-dependency-path: labs/12-product-engineering-loop/product-engineering-loop/go.mod
cache-dependency-path: labs/12-product-engineering-loop/product-engineering-loop/go.sum
- name: Show toolchain
run: go version
- name: Test runtime
Expand Down Expand Up @@ -66,10 +66,10 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/12-product-engineering-loop/product-engineering-loop/go.mod
cache-dependency-path: labs/12-product-engineering-loop/product-engineering-loop/go.mod
cache-dependency-path: labs/12-product-engineering-loop/product-engineering-loop/go.sum
- uses: actions/setup-python@v6
with:
python-version: "3.11"
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/ci-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI speed policy

# Structural enforcement of the Go CI speed policy: every job that compiles Go
# must go through the shared .github/actions/go-setup action (module + build
# caching, Windows Defender exclusions). See .github/scripts/lint_go_ci_policy.py.
on:
push:
branches: [main]
paths:
- ".github/workflows/**"
- ".github/actions/go-setup/**"
- ".github/scripts/lint_go_ci_policy.py"
pull_request:
paths:
- ".github/workflows/**"
- ".github/actions/go-setup/**"
- ".github/scripts/lint_go_ci_policy.py"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-policy-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
go-caching-policy:
name: Go caching policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install PyYAML
run: pip install pyyaml
- name: Enforce Go CI speed policy
run: python .github/scripts/lint_go_ci_policy.py
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
working-directory: conformance
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: conformance/go.mod
cache-dependency-path: conformance/go.mod
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deltawire-v7.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v6
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/20-deltawire/go.mod
cache-dependency-path: labs/20-deltawire/go.sum
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deltawire-v8.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v6
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/20-deltawire/go.mod
cache-dependency-path: labs/20-deltawire/go.sum
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/pitot-lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ jobs:
working-directory: labs/15-pitot/pitot
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.sum
- name: Show toolchain
run: go version
# The module must stand on its own, independent of the workspace, so the
Expand All @@ -60,10 +60,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.sum
# Runs from the repo root so the committed go.work binds every co-resident
# module at HEAD. This is where the future Boatstack <-> Pitot leg lands.
- name: Integrated HEAD check
Expand All @@ -74,10 +74,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.sum
- name: Enforce sensor does not import bridge
run: bash labs/15-pitot/scripts/check_import_boundary.sh

Expand All @@ -88,10 +88,10 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.sum
- uses: actions/setup-python@v6
with:
python-version: "3.11"
Expand Down
Loading