Skip to content

Commit ac7df3f

Browse files
ci(perf): stop macOS-on-PR, enable Go cache, least-privilege (#481)
macOS runners are the biggest CI cost multiplier (10x ubuntu-latest minutes) and stem's build matrix built both darwin rows on every PR despite only darwin/arm64 ever shipping in release.yml. Since stem is CGO_ENABLED=0, drop both darwin rows from the PR build matrix and replace them with a cheap cross-compile smoke check on ubuntu-latest. Also enable keyed Go module/build caching (copied from seed's setup-go composite action), narrow ci.yml's top-level permissions from broad pull-requests/checks/security-events writes down to contents:read + a scoped grant on the one job that needs SARIF upload, and pin release.yml's syft install to a checksummed release archive instead of piping install.sh from the mutable main branch. Related to #480
1 parent 5dbe869 commit ac7df3f

3 files changed

Lines changed: 109 additions & 18 deletions

File tree

.github/actions/setup-go/action.yml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: 'Setup Go'
2-
description: 'Set up Go with caching and module download'
2+
description: 'Set up Go with build/module caching and module download'
33
inputs:
44
go-version-file:
55
description: 'Path to go.mod file'
@@ -12,8 +12,45 @@ runs:
1212
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
1313
with:
1414
go-version-file: ${{ inputs.go-version-file }}
15+
# setup-go's built-in cache keys only on go.sum and caches the default
16+
# GOCACHE; the race job overrides GOCACHE/GOMODCACHE to the workspace,
17+
# so we manage the cache explicitly below to capture the *build*
18+
# cache too (mirrors seed's setup-go composite action).
1519
cache: false
1620

21+
# Resolve the active build/module cache dirs. These honor any job-level
22+
# GOCACHE/GOMODCACHE override (the race job points them at the workspace).
23+
- name: Resolve Go cache paths
24+
id: go-cache-paths
25+
shell: bash
26+
run: |
27+
build="$(go env GOCACHE)"
28+
mod="$(go env GOMODCACHE)"
29+
# Jobs that override GOCACHE/GOMODCACHE to the workspace must not share
30+
# a cache key with default-path jobs (actions/cache restores to the
31+
# saved path, so a path mismatch leaves the job cold). Discriminate by
32+
# path.
33+
tag="$(printf '%s|%s' "$build" "$mod" | sha256sum | cut -c1-12)"
34+
{
35+
echo "build=$build"
36+
echo "mod=$mod"
37+
echo "tag=$tag"
38+
} >> "$GITHUB_OUTPUT"
39+
40+
# Persist the build + module caches across runs (official actions/cache Go
41+
# pattern: key on go.sum, fall back by prefix). `tag` discriminates the
42+
# cache by path so workspace-GOCACHE jobs don't collide with default-path
43+
# jobs on a shared key.
44+
- name: Cache Go build and module caches
45+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
46+
with:
47+
path: |
48+
${{ steps.go-cache-paths.outputs.build }}
49+
${{ steps.go-cache-paths.outputs.mod }}
50+
key: ${{ runner.os }}-go-${{ steps.go-cache-paths.outputs.tag }}-${{ hashFiles('**/go.sum') }}
51+
restore-keys: |
52+
${{ runner.os }}-go-${{ steps.go-cache-paths.outputs.tag }}-
53+
1754
- name: Download Go modules
1855
shell: bash
1956
run: go mod download

.github/workflows/ci.yml

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ concurrency:
1717
group: ${{ github.workflow }}-${{ github.ref }}
1818
cancel-in-progress: true
1919

20+
# Least-privilege default: every job gets read-only contents unless it
21+
# specifically needs more (granted at the job level below). None of these
22+
# jobs comment on or annotate PRs via the GitHub API, so pull-requests/checks
23+
# write were unused broad grants — only the `security` job's SARIF upload
24+
# needs security-events:write (+ actions:read, required for private repos
25+
# per github/codeql-action/upload-sarif's docs; niac hit this exact gap).
2026
permissions:
21-
actions: read
2227
contents: read
23-
pull-requests: write
24-
checks: write
25-
security-events: write
2628

2729
jobs:
2830
# ============================================================================
@@ -441,6 +443,10 @@ jobs:
441443
name: Security Scanning
442444
runs-on: ubuntu-latest
443445
# Always runs — security checks are non-negotiable on every PR.
446+
permissions:
447+
contents: read
448+
security-events: write # upload-sarif (gosec + Trivy)
449+
actions: read # required alongside security-events:write on private repos
444450

445451
steps:
446452
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -639,6 +645,14 @@ jobs:
639645
# isn't bound until after the strategy expands). All matrix rows run
640646
# on every PR; per-arch arches are fast enough that the savings weren't
641647
# worth a workaround.
648+
#
649+
# macOS runners are 10x the minute multiplier of ubuntu-latest and are
650+
# NOT built here on PRs — darwin/amd64 (Intel) is dropped fleet-wide,
651+
# and darwin/arm64's real release binary is produced by goreleaser in
652+
# release.yml (unchanged). Since stem is CGO_ENABLED=0 (pure Go), the
653+
# darwin/arm64 build cross-compiles trivially on ubuntu; the dedicated
654+
# darwin-compile-check job below verifies it still compiles without
655+
# paying for a macOS runner on every PR.
642656
if: |
643657
always() &&
644658
needs.frontend.result == 'success' &&
@@ -659,18 +673,6 @@ jobs:
659673
goos: linux
660674
goarch: arm64
661675
required: false
662-
- os: darwin
663-
arch: amd64
664-
runner: macos-latest
665-
goos: darwin
666-
goarch: amd64
667-
required: false
668-
- os: darwin
669-
arch: arm64
670-
runner: macos-latest
671-
goos: darwin
672-
goarch: arm64
673-
required: false
674676
steps:
675677
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
676678

@@ -747,6 +749,44 @@ jobs:
747749
path: bin/stem-${{ matrix.os }}-${{ matrix.arch }}
748750
retention-days: 7
749751

752+
# ============================================================================
753+
# Darwin compile check — verifies the release target still cross-compiles
754+
# ============================================================================
755+
# macOS runners cost 10x the ubuntu-latest minute multiplier, so darwin
756+
# binaries are no longer built on PRs at all (see the `build` job comment
757+
# above). stem is CGO_ENABLED=0 (pure Go), so GOOS=darwin GOARCH=arm64
758+
# cross-compiles cleanly on ubuntu-latest — this job is a cheap compile-only
759+
# smoke check that the darwin/arm64 target hasn't broken, without paying for
760+
# a macOS runner. The actual signed darwin/arm64 release binary is still
761+
# produced by goreleaser in release.yml, unchanged. darwin/amd64 (Intel) is
762+
# dropped fleet-wide and is not checked here.
763+
darwin-compile-check:
764+
name: Darwin compile check (arm64)
765+
runs-on: ubuntu-latest
766+
needs: [changes, backend, frontend]
767+
if: |
768+
always() &&
769+
needs.frontend.result == 'success' &&
770+
(needs.backend.result == 'success' || needs.backend.result == 'skipped')
771+
steps:
772+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
773+
774+
- name: Set up Go
775+
uses: ./.github/actions/setup-go
776+
777+
- name: Download frontend build
778+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
779+
with:
780+
name: frontend-dist
781+
path: internal/api/ui/
782+
783+
- name: Cross-compile for darwin/arm64 (no macOS runner needed)
784+
env:
785+
GOOS: darwin
786+
GOARCH: arm64
787+
CGO_ENABLED: '0'
788+
run: go build -trimpath -o /dev/null ./cmd/stem/
789+
750790
# ============================================================================
751791
# Lighthouse Performance Audit
752792
# ============================================================================
@@ -933,6 +973,7 @@ jobs:
933973
- i18n
934974
- docs
935975
- build
976+
- darwin-compile-check
936977
if: always()
937978
steps:
938979
- name: Verify upstream results

.github/workflows/release.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,21 @@ jobs:
136136
path: internal/api/ui/
137137

138138
- name: Install Syft (SBOM) inside container
139+
env:
140+
# Pinned version + checksum (from syft's published checksums.txt),
141+
# matching the gitleaks-CLI pattern in ci.yml's security job —
142+
# `curl ... install.sh | sh` from the mutable `main` branch is a
143+
# supply-chain risk (a compromised main would run arbitrary code
144+
# in the release container). Rotate together on upgrade.
145+
SYFT_VERSION: "1.46.0"
146+
SYFT_SHA256: "d654f678b709eb53c393d38519d5ed7d2e57205529404018614cfefa0fb2b5ca"
139147
run: |
140-
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
148+
set -euo pipefail
149+
curl -sSfL -o syft.tar.gz \
150+
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz"
151+
echo "${SYFT_SHA256} syft.tar.gz" | sha256sum -c -
152+
tar -xzf syft.tar.gz -C /usr/local/bin syft
153+
chmod +x /usr/local/bin/syft
141154
142155
- name: Install Cosign inside container
143156
run: |

0 commit comments

Comments
 (0)