Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
67bc4ca
ci: split ci into a job dag and isolate docs deploys from pr code
EricAndrechek Jun 10, 2026
c321e40
ci: drop persisted git credentials from every ci.yml checkout
EricAndrechek Jun 10, 2026
4cdde05
ci: make e2e self-building and fold coverage gate into its tail
EricAndrechek Jun 10, 2026
72ab13e
docs: align changelog ci entry with the reshaped job dag
EricAndrechek Jun 10, 2026
17acc62
ci: drop per-job setup-go for system go + cached auto toolchain
EricAndrechek Jun 10, 2026
ed1db1c
ci: shard e2e across isolated stacks and cut every needs edge on the …
EricAndrechek Jun 10, 2026
9a20d66
ci: own the architecture — scripts, one canonical doc, linted plumbing
EricAndrechek Jun 10, 2026
6778b17
ci: drop e2e sharding for local parity and simplify the hot path
EricAndrechek Jun 10, 2026
e22c7e4
ci: run on merge_group so prs can land through a merge queue
EricAndrechek Jun 10, 2026
d9f9f44
ci: standalone coverage job, shared classifier, change-aware pre-push
EricAndrechek Jun 10, 2026
acaf8dd
Merge remote-tracking branch 'origin/main' into speedy-ci
EricAndrechek Jun 10, 2026
25d75f6
docs: fix stale coverage minima and bump ci timing to measured ~4m
EricAndrechek Jun 10, 2026
f54a482
ci: overlap the coverage job's setup with the suites via a poll
EricAndrechek Jun 10, 2026
3070f45
Merge remote-tracking branch 'origin/main' into speedy-ci
EricAndrechek Jun 10, 2026
4a35e4f
ci: make the docs-preview deploy non-gating
EricAndrechek Jun 10, 2026
7f859e4
ci: fix pr-template allowlist casing in the change classifier
EricAndrechek Jun 10, 2026
43a34cf
Merge remote-tracking branch 'origin/main' into speedy-ci
EricAndrechek Jun 10, 2026
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
2 changes: 1 addition & 1 deletion .claude/hooks/agent-bash-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+pr[[:s
fi

# gh pr create / gh pr edit --title: validate the title against the SAME
# Conventional-Commits rule the required `PR housekeeping` check enforces
# Conventional-Commits rule the required `CI` check's `PR title` job enforces
# (scripts/lint-pr-title.sh is the shared rule) — so a too-long or wrong-format
# title is caught locally BEFORE the PR exists, not after the required check
# fails. Extract the quoted --title/-t value from the ORIGINAL command (the
Expand Down
69 changes: 61 additions & 8 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,21 +1,74 @@
#!/usr/bin/env bash
# Blocks `git push` unless `make ci` validated the tree of each ref tip.
# Tree-keyed. Bypass: `git push --no-verify` (humans only).
# Blocks `git push` unless the tree of each pushed ref tip was validated
# locally. The bar scales with what changed — classified by the SAME
# allowlist CI uses (scripts/classify-paths.sh), so local and CI agree on
# what a change "is":
# - any code change → full `make ci` must have passed (tree-keyed marker
# tmp/ci-passed-tree-<tree>), exactly as before;
# - docs/prose-only → `make verify` is enough (CI skips the Go/SDK suites
# for these too), so only the verify marker is required — no spinning
# up the local Docker suites for a typo fix. pre-commit already runs
# `make verify`, so this is usually a zero-extra-work push.
# Fail-closed: if the change set can't be classified (no resolvable base,
# classifier error), fall back to requiring the full `make ci` marker.
# Bypass: `git push --no-verify` (humans only).

set -uo pipefail
cd "$(git rev-parse --show-toplevel)" || exit 1

while read -r _ local_sha _ _; do
[ -z "${local_sha:-}" ] && continue
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
marker=$(scripts/ci-marker.sh path-for-commit "$local_sha" 2>/dev/null) || {
echo "🛑 pre-push: can't resolve tree for ${local_sha:0:8}." >&2; exit 1
zero=0000000000000000000000000000000000000000

# Net changed files for a pushed ref (base..tip), one per line. New branch
# (no remote tip) → diff against the merge-base with origin/main. Returns
# non-zero when no base resolves, so the caller fails closed.
changed_files() {
local tip="$1" remote="$2" base
if [ "$remote" != "$zero" ]; then
base="$remote"
else
base="$(git merge-base origin/main "$tip" 2>/dev/null)" || return 1
fi
git diff --name-only "$base" "$tip" 2>/dev/null
}

block_ci() { # <sha> — require the full make-ci marker
local sha="$1" marker tree
marker=$(scripts/ci-marker.sh path-for-commit "$sha" 2>/dev/null) || {
echo "🛑 pre-push: can't resolve tree for ${sha:0:8}." >&2; exit 1
}
if [ ! -f "$marker" ]; then
tree="${marker#tmp/ci-passed-tree-}"
echo "🛑 pre-push: 'make ci' has not been run for ${local_sha:0:8} (tree ${tree:0:8}). Run 'make ci' or 'git push --no-verify'." >&2
echo "🛑 pre-push: 'make ci' has not been run for ${sha:0:8} (tree ${tree:0:8}). Run 'make ci' or 'git push --no-verify'." >&2
exit 1
fi
}

while read -r _ local_sha _ remote_sha; do
[ -z "${local_sha:-}" ] && continue
[ "$local_sha" = "$zero" ] && continue # branch deletion — nothing to validate

# Classify the push, failing closed to a code change on any hiccup.
code=true
if files="$(changed_files "$local_sha" "${remote_sha:-$zero}")"; then
verdict="$(printf '%s\n' "$files" | scripts/classify-paths.sh 2>/dev/null || true)"
case "$verdict" in
*code=false*) code=false ;;
esac
fi

if [ "$code" = false ]; then
marker=$(scripts/ci-marker.sh verify-path-for-commit "$local_sha" 2>/dev/null) || {
echo "🛑 pre-push: can't resolve tree for ${local_sha:0:8}." >&2; exit 1
}
if [ ! -f "$marker" ]; then
echo "🛑 pre-push: docs-only push, but 'make verify' hasn't passed for ${local_sha:0:8}. Run 'make verify' (no Docker) or 'git push --no-verify'." >&2
exit 1
fi
echo "✔ pre-push: docs/prose-only change — 'make verify' marker present for ${local_sha:0:8} (CI skips the suites too)." >&2
continue
fi

block_ci "$local_sha"
done

exit 0
158 changes: 106 additions & 52 deletions .github/actions/setup-env/action.yml
Original file line number Diff line number Diff line change
@@ -1,69 +1,107 @@
# Restores caches + sets up Go/pnpm/Node for `make ci`. Composite
# actions can't self-checkout, so ci.yml must checkout first. Saves
# live in ci.yml gated on the `*-cache-hit` outputs below.
# Caches + toolchains for the CI jobs, in one place. Composite actions
# can't self-checkout, so the consuming job must checkout first. Each CI
# job opts into exactly the toolchains it needs via the inputs below.
#
# 1. Go module + build cache keyed on every go.sum. `restore-keys`
# keeps the build cache warm across go.sum bumps — avoids
# setup-go's exact-key-only cache, see actions/setup-go#357.
# Caching is OWNED here end-to-end: each cache is a nested `actions/cache`
# step, whose automatic post-job step saves on an exact-key miss — no save
# steps in ci.yml, so restore and save can never drift. Two consequences
# to know about (both accepted for the boilerplate this kills):
# - saves happen at the END of the job (post steps), and only when the
# job SUCCEEDED — a failed run rebuilds from restore-keys next time;
# - when several jobs miss the same key in one run (e.g. a lockfile
# bump), each saves; the backend keeps the first and the rest log a
# benign "already exists" warning.
#
# The cache inventory (full architecture: .github/workflows/README.md):
#
# 1. Go module + build cache keyed on every go.sum, `gobuild-v2-` key
# family. `restore-keys` keeps the build cache warm across go.sum
# bumps — avoids setup-go's exact-key-only cache (actions/setup-go#357).
# `go-cache-suffix` partitions the cache per job: the unit,
# integration, and e2e jobs compile with different flags
# (-race/-cover/-coverpkg/-tags), so a shared entry would only ever
# be warm for whichever job saved it. Cross-suffix restore-keys
# still share the (identical) module cache on a cold start.
# 2. golangci-lint binary + analysis cache keyed on Makefile +
# .golangci.yml. Analysis cache is the win (~10s warm vs ~90s).
#
# Only the lint job needs it.
# 3. pnpm store (path resolved at runtime) keyed on the root
# lockfile — the three pnpm projects (clients/ts, tests/e2e/sdk,
# docs) share one workspace lockfile + store. Path is dynamic because
# pnpm's documented default ~/.local/share/pnpm/store only
# applies when $HOME and the project tree share a mount; on some
# runners it falls back to a workspace-relative path. Hard-coding
# the default silently fails the save with a Path Validation
# Error.
#
# lockfile — the pnpm workspace projects share one lockfile +
# store. Path is dynamic because pnpm's documented default
# ~/.local/share/pnpm/store only applies when $HOME and the project
# tree share a mount; on some runners it falls back to a
# workspace-relative path. Hard-coding the default silently fails
# the save with a Path Validation Error.
# 4. Playwright browser cache (~/.cache/ms-playwright) keyed on the
# root lockfile. ~130 MB Chromium download otherwise re-fetched
# every run. See #132.
#
# every docs build (rehype-mermaid renders via headless Chrome).
# Only the docs-build job needs it. See #132.
# 5. Astro content-collection cache (docs/.astro/) keyed on the root
# lockfile + astro.config.mjs. Speeds up warm `astro build` —
# unchanged content skips the parse + transform pipeline. See
# #132.
# lockfile + astro.config.mjs. Speeds up warm `astro check` /
# `astro build` — unchanged content skips the parse + transform
# pipeline. See #132.
name: Setup CI environment
description: Restores caches and sets up Go + pnpm + Node for WaveHouse CI
description: Caches (restore + automatic post-job save) and toolchains for WaveHouse CI

# Cache-hit flags + resolved pnpm store path surfaced to ci.yml.
inputs:
go:
description: "Set up the Go toolchain + the Go module/build cache"
default: "true"
go-cache-suffix:
description: "Per-job Go build-cache partition, e.g. '-unit' (different jobs compile with different flags). Empty = the shared default key."
default: ""
golangci:
description: "Cache the golangci-lint binary + analysis cache (lint job only)"
default: "false"
node:
description: "Set up pnpm + Node and cache the pnpm store"
default: "true"
playwright:
description: "Cache the Playwright Chromium install (docs build only)"
default: "false"
astro:
description: "Cache the Astro content collections (docs check/build)"
default: "false"

# Exact-key cache-hit flags, for consumers that want to skip work on a
# warm cache (e.g. docs-build's pnpm-store prune). Saves are NOT gated on
# these from the outside — the nested actions/cache steps handle their
# own post-job saves.
outputs:
gobuild-cache-hit:
description: "true if the exact-key Go module + build cache was restored"
value: ${{ steps.gobuild-cache.outputs.cache-hit }}
golangci-cache-hit:
description: "true if the exact-key golangci-lint cache was restored"
value: ${{ steps.golangci-cache.outputs.cache-hit }}
pnpm-cache-hit:
description: "true if the exact-key pnpm store cache was restored"
value: ${{ steps.pnpm-cache.outputs.cache-hit }}
pnpm-store-path:
description: "Absolute path to the pnpm content-addressable store on this runner — pass to actions/cache/save in the consuming workflow."
value: ${{ steps.pnpm-store.outputs.path }}
playwright-cache-hit:
description: "true if the exact-key Playwright browser cache was restored"
value: ${{ steps.playwright-cache.outputs.cache-hit }}
astro-cache-hit:
description: "true if the exact-key Astro content cache was restored"
value: ${{ steps.astro-cache.outputs.cache-hit }}

runs:
using: composite
steps:
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
# Key version (v2): bumped when the cache's expected CONTENTS change
# shape — v2 added the go toolchain itself (~/go/pkg/mod/golang.org/
# toolchain, see the GOTOOLCHAIN note below). Saves only happen on an
# exact-key miss, so without a bump the pre-change entry would
# exact-hit forever and the new content would never be saved.
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.go == 'true' }}
id: gobuild-cache
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: gobuild-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
key: gobuild-v2-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-${{ hashFiles('**/go.sum') }}
# Same-suffix prefix first (this job's flavor across go.sum bumps),
# then the bare prefix as a cold-start fallback — it matches any
# other job's suffixed entry, which still carries the shared module
# cache even if its build objects don't apply. The unversioned (v1)
# prefixes are transitional warm-start fallbacks; drop them once a
# v2 entry has been saved on main.
restore-keys: |
gobuild-v2-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-
gobuild-v2-${{ runner.os }}-go-
gobuild-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-
gobuild-${{ runner.os }}-go-

- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.golangci == 'true' }}
id: golangci-cache
with:
path: |
Expand All @@ -73,25 +111,38 @@ runs:
restore-keys: |
golangci-${{ runner.os }}-

# `cache: false` — Go cache is managed explicitly above.
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache: false
# No actions/setup-go: it spends ~9s/job downloading + extracting a
# toolchain the runner can already provide. The image's preinstalled
# `go` + GOTOOLCHAIN=auto (the Go ≥1.21 default) resolve go.mod's
# pinned version exactly — and the auto-fetched toolchain lands in
# ~/go/pkg/mod/golang.org/toolchain, i.e. inside the module cache
# restored above, so warm runs download nothing. A cold cache (first
# run after a key rotation, when the image go is older than go.mod's
# pin) pays a one-time toolchain fetch from proxy.golang.org here —
# this step makes that cost visible and the post-job save captures it.
# Trade-off: setup-go's inline problem matchers (PR-file annotations
# on compile errors) are gone; the log output is unchanged.
- name: Verify Go resolves go.mod's toolchain
if: ${{ inputs.go == 'true' }}
shell: bash
run: go version

# pnpm must install before its cache restore so the next step can
# ask pnpm for the actual store path.
# pnpm must install before its cache step so the store path can be
# resolved from pnpm itself.
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
if: ${{ inputs.node == 'true' }}
with:
version: "11.1.3"
run_install: false

- name: Resolve pnpm store directory
if: ${{ inputs.node == 'true' }}
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.node == 'true' }}
id: pnpm-cache
with:
path: ${{ steps.pnpm-store.outputs.path }}
Expand All @@ -100,11 +151,12 @@ runs:
pnpm-${{ runner.os }}-

# Playwright Chromium binary (~130 MB) lives outside the pnpm store at
# ~/.cache/ms-playwright. Without this cache, every CI run cold-pulls
# ~/.cache/ms-playwright. Without this cache, every docs build cold-pulls
# Chromium during install-playwright-docs — adds 30-60s per run. Keyed
# on the root pnpm-lock.yaml since that's where the Playwright version
# is locked. Closes the Playwright half of #132.
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.playwright == 'true' }}
id: playwright-cache
with:
path: ~/.cache/ms-playwright
Expand All @@ -113,13 +165,14 @@ runs:
playwright-${{ runner.os }}-

# Astro's content-collection cache (docs/.astro/data-store.json plus
# generated content types). Restored across runs so unchanged Markdown
# generated content types). Kept across runs so unchanged Markdown
# / MDX content skips the parse + transform pipeline. Keyed on
# lockfile + astro.config.mjs since plugin-chain changes invalidate
# the cache shape; intra-key content changes are caught by Astro's
# own file-hash check inside data-store.json. Closes the build-cache
# half of #132.
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ inputs.astro == 'true' }}
id: astro-cache
with:
path: docs/.astro
Expand All @@ -128,5 +181,6 @@ runs:
astro-${{ runner.os }}-

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
if: ${{ inputs.node == 'true' }}
with:
node-version-file: ".nvmrc"
Loading
Loading