Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
40 changes: 38 additions & 2 deletions .github/actions/setup-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
# via restore-keys and fetches the delta on every push, for its whole life.
# golangci and Astro still save from any ref (see the inventory).
#
# One exception, for the Go build cache of the two long-pole jobs (the
# integration suite's `app` part and e2e): on a pull request that changes a
# go.mod/go.sum, ci.yml sets `go-cache-pr-save` from the `changes` job's
# `go-deps`, and the cache saves into the PR's own scope. The key is per
# dependency state, so the PR saves once and its later pushes restore their
# own entry instead of recompiling the dependency delta, about 30-45s on
# each long pole. pr-cache-cleanup.yml deletes the entries no run will read.
#
# The cache inventory (full architecture: .github/workflows/README.md):
#
# 1. Go modules, `gomod-v1-` key family, keyed on every go.mod + go.sum
Expand Down Expand Up @@ -68,6 +76,9 @@ inputs:
go-cache-suffix:
description: "REQUIRED when `go` is true (the default) — the action fails the job without one. Per-job Go build-cache partition, e.g. '-unit', fresh for every job (jobs compile with different flags, and two jobs on one key race to save it); an empty suffix yields a restore-key that prefix-matches every other flavor's entry, the cross-flavor restore the gomod-v1/gobuild-v3 split exists to avoid. A job that doesn't build Go passes `go: \"false\"` instead. The shared part of the cache is gomod-v1, which needs no suffix. The default stays empty because a composite action's `required:` isn't enforced by the runner — the guard step is."
default: ""
go-cache-pr-save:
description: "Also save this job's Go build cache from a pull request, into the PR's own scope, when its key misses. ci.yml sets it only for the long-pole jobs of a PR that changes a go.mod/go.sum (the `changes` job's `go-deps`): see the sizing policy in .github/workflows/README.md before adding a job."
default: "false"
golangci:
description: "Cache the golangci-lint binary + analysis cache (lint job only)"
default: "false"
Expand Down Expand Up @@ -171,7 +182,8 @@ runs:
# miss, so without a bump the old v2 entry would exact-hit forever and
# the new, smaller content would never be saved.
#
# Save-on-`main` only, like gomod-v1. Go trims build-cache entries
# Save-on-`main`, like gomod-v1, apart from the long poles on a
# dependency-changing PR (header, and below). Go trims build-cache entries
# unused for five days on its own (mtimes survive the archive), so the
# restore -> save chain on `main` doesn't grow without bound.
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down Expand Up @@ -199,14 +211,38 @@ runs:
gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-

- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.go == 'true' && github.ref != 'refs/heads/main' }}
if: ${{ inputs.go == 'true' && github.ref != 'refs/heads/main' && !(github.event_name == 'pull_request' && inputs.go-cache-pr-save == 'true') }}
id: gobuild-cache-restore
with:
path: ~/.cache/go-build
key: gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-${{ hashFiles('**/go.mod', '**/go.sum') }}
restore-keys: |
gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-

# `go-cache-pr-save` on a pull request (the header's exception). The
# exact key first, from the PR's scope or main's: a PR whose dependency
# change main already has hits main's entry here and saves nothing. Only
# on a miss does the step below restore the newest entry of this flavor
# (the PR's own, then main's) and save the exact key into the PR's
# scope. One step with restore-keys would prefer a stale entry in the
# PR's scope, left from an earlier dependency change, to main's exact one.
# The prefix must match scripts/ci/prune-pr-build-cache.sh.
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.go == 'true' && github.event_name == 'pull_request' && inputs.go-cache-pr-save == 'true' }}
id: gobuild-cache-pr-exact
with:
path: ~/.cache/go-build
key: gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-${{ hashFiles('**/go.mod', '**/go.sum') }}

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.go == 'true' && github.event_name == 'pull_request' && inputs.go-cache-pr-save == 'true' && steps.gobuild-cache-pr-exact.outputs.cache-hit != 'true' }}
id: gobuild-cache-pr
with:
path: ~/.cache/go-build
key: gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-${{ hashFiles('**/go.mod', '**/go.sum') }}
restore-keys: |
gobuild-v3-${{ runner.os }}-go${{ inputs.go-cache-suffix }}-

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.golangci == 'true' }}
id: golangci-cache
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/README.md

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ concurrency:
jobs:
# ── Change detection ───────────────────────────────────────────────
# Classifies the changed files (scripts/ci/classify-changes.sh — the
# single source of truth for the code/docs outputs and their fail-closed
# rules) so the test/docs jobs can skip work, and lists the integration
# single source of truth for the code/docs/go-deps outputs and their
# fail-closed rules) so the test/docs jobs can skip work and the long poles
# know whether a PR changes its Go dependencies, and lists the integration
# suite's parts for the integration matrix and the coverage job.
changes:
name: Detect changes
Expand All @@ -74,6 +75,7 @@ jobs:
outputs:
code: ${{ steps.detect.outputs.code }}
docs: ${{ steps.detect.outputs.docs }}
go-deps: ${{ steps.detect.outputs.go-deps }}
integration-parts: ${{ steps.parts.outputs.integration-parts }}
integration-fragments: ${{ steps.parts.outputs.integration-fragments }}
integration-producers: ${{ steps.parts.outputs.integration-producers }}
Expand Down Expand Up @@ -261,6 +263,10 @@ jobs:
uses: ./.github/actions/setup-env
with:
go-cache-suffix: "-integration-${{ matrix.part }}"
# Only the long pole saves, and only on a PR that changes a
# go.mod/go.sum (README, sizing policy): `backends` finishes well
# inside it either way.
go-cache-pr-save: ${{ matrix.part == 'app' && needs.changes.outputs.go-deps == 'true' }}
node: "false" # pure Go + testcontainers
- name: Run Go integration tests
env:
Expand Down Expand Up @@ -306,6 +312,7 @@ jobs:
uses: ./.github/actions/setup-env
with:
go-cache-suffix: "-e2e-cov"
go-cache-pr-save: ${{ needs.changes.outputs.go-deps == 'true' }} # the other long pole
# `-j` builds the prereqs (build-ts ∥ build-cover) concurrently,
# then runs the orchestrator: ClickHouse and Redis testcontainers +
# the cover binary + the SDK vitest suite.
Expand Down
53 changes: 45 additions & 8 deletions .github/workflows/pr-cache-cleanup.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
name: PR cache cleanup

# When a PR closes (merged or not), delete the Actions caches saved under its
# refs. The Go, pnpm and Playwright caches only save from `main`; what a PR
# still saves is GitHub's CodeQL dependency cache (~0.4-0.6 GiB, whenever main
# has no entry for its key), the small golangci/Astro entries, and any
# Go caches saved before that change. None is readable by another PR, and they
# count against the repo's 10 GB cache cap until evicted.
# Deletes the Actions caches pull requests no longer need. Both jobs run the
# default branch's copy of this workflow and execute nothing from a PR, so no
# PR-authored code holds the write token, and fork PRs are covered too.
#
# No checkout and nothing from the PR is executed: the only PR-derived input is
# the number, passed through the environment.
# - When a PR closes (merged or not): every cache saved under its refs. The
# Go, pnpm and Playwright caches save only from `main`, except the build
# cache below; what a PR still saves is GitHub's CodeQL dependency cache
# (~0.4-0.6 GiB, whenever main has no entry for its key), the small
# golangci/Astro entries, and any Go caches saved before that change. None
# is readable by another PR, and they count against the repo's 10 GB cache
# cap until evicted. No checkout: the only PR-derived input is the number,
# passed through the environment.
# - After each CI run of a pull request: the Go build cache entries no run
# will read. A PR that changes go.mod/go.sum saves the build cache of the
# two long-pole jobs into its own scope (setup-env's `go-cache-pr-save`),
# and scripts/ci/prune-pr-build-cache.sh keeps at most one per job per PR.

on:
pull_request_target:
types: [closed]
workflow_run:
workflows: [CI]
types: [completed]

permissions:
actions: write

jobs:
cache-cleanup:
name: Delete PR caches
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -36,3 +47,29 @@ jobs:
gh cache delete --all --ref "$ref" --succeed-on-no-caches || rc=1
done
exit "$rc"

build-cache-prune:
name: Prune PR build caches
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
# Overlapping prunes would race to delete the same entries. A newer
# pending run replaces an older one, which is fine: each prunes everything.
concurrency:
group: pr-build-cache-prune
permissions:
actions: write
contents: read
steps:
# The default branch, never the PR's tree: the script runs with the
# write token.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 1
persist-credentials: false
- name: Delete the build caches no run will read
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: scripts/ci/prune-pr-build-cache.sh
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ docs/ → Project documentation

## Repository Automation

- **CI** (`ci.yml`): a job DAG over the same Makefile targets as local `make ci` (~3m30s–4m push → green). **The architecture doc is [`.github/workflows/README.md`](.github/workflows/README.md)** — DAG diagram, design invariants, cache policy, and the add-a-job recipe; read it before editing `ci.yml`. The load-bearing facts: the `changes` job (`scripts/ci/classify-changes.sh`, fail-closed) gates the test/docs jobs; the `e2e` job, one of the two long poles, builds its own SDK dist + cover binary and runs the suite exactly like local `make test-e2e`; the integration suite runs one matrix job per part of `make test-integration` (the Makefile's `INTEGRATION_PARTS`, read by `scripts/ci/integration-parts.sh`; its `app` part is the other long pole); each suite uploads a `coverage-<suite>` fragment (the integration suite one per part, `coverage-integration-<part>`) and a dedicated `coverage` job merges them + applies every threshold gate via `make cov` (like local `make ci`'s final step — kept separate so the gate is decoupled from the e2e suite; it's `needs: changes` only and *polls* for the fragments via `scripts/ci/wait-artifact.sh` rather than `needs`-ing the suites, so its ~50s setup overlaps them and the merge fires ~10s after the last suite instead of serializing setup onto the critical path); an **aggregator job named `CI`** is the ruleset's sole required status check (fails on failed/cancelled needs, treats skipped as passing) — the Cloudflare `docs-preview` deploy is deliberately NOT a need (non-gating, like `timing`: only `docs-build` gates, so a slow/failed preview never delays or reds the required check); caches are owned end-to-end by `.github/actions/setup-env` (nested `actions/cache`, automatic post-job saves — never add save steps to `ci.yml`); the Go, pnpm and Playwright caches save only on `main`, and every other ref uses `actions/cache/restore`; `pr-cache-cleanup.yml` deletes a closed PR's caches. Plain `make build` binaries are not linked in CI — compile breakage is caught by lint/tests/the cover-binary link, release builds by `goreleaser-validate.yml` / `publish-dev.yml`. Fork PRs run the full secretless pipeline; merge-queue `merge_group` runs re-test the full suite against current main (the queue replaces the old require-up-to-date rule — never remove the `merge_group:` trigger, or queued PRs hang). CI logic lives in `scripts/ci/*.sh`, gated by `make lint-sh` (shellcheck) and `make lint-gha` (actionlint) inside `make verify` — not in inline YAML, except in the trusted-main deploy jobs where inline is the trust boundary.
- **CI** (`ci.yml`): a job DAG over the same Makefile targets as local `make ci` (~3m30s–4m push → green). **The architecture doc is [`.github/workflows/README.md`](.github/workflows/README.md)** — DAG diagram, design invariants, cache policy, and the add-a-job recipe; read it before editing `ci.yml`. The load-bearing facts: the `changes` job (`scripts/ci/classify-changes.sh`, fail-closed) gates the test/docs jobs; the `e2e` job, one of the two long poles, builds its own SDK dist + cover binary and runs the suite exactly like local `make test-e2e`; the integration suite runs one matrix job per part of `make test-integration` (the Makefile's `INTEGRATION_PARTS`, read by `scripts/ci/integration-parts.sh`; its `app` part is the other long pole); each suite uploads a `coverage-<suite>` fragment (the integration suite one per part, `coverage-integration-<part>`) and a dedicated `coverage` job merges them + applies every threshold gate via `make cov` (like local `make ci`'s final step — kept separate so the gate is decoupled from the e2e suite; it's `needs: changes` only and *polls* for the fragments via `scripts/ci/wait-artifact.sh` rather than `needs`-ing the suites, so its ~50s setup overlaps them and the merge fires ~10s after the last suite instead of serializing setup onto the critical path); an **aggregator job named `CI`** is the ruleset's sole required status check (fails on failed/cancelled needs, treats skipped as passing) — the Cloudflare `docs-preview` deploy is deliberately NOT a need (non-gating, like `timing`: only `docs-build` gates, so a slow/failed preview never delays or reds the required check); caches are owned end-to-end by `.github/actions/setup-env` (nested `actions/cache`, automatic post-job saves — never add save steps to `ci.yml`); the Go, pnpm and Playwright caches save only on `main`, and every other ref uses `actions/cache/restore`, except that the two long poles (`go-cache-pr-save`) save their Go build cache into a PR's scope when the PR changes `go.mod`/`go.sum`; `pr-cache-cleanup.yml` deletes a closed PR's caches and, after each PR run, the build cache entries no run will read. Plain `make build` binaries are not linked in CI — compile breakage is caught by lint/tests/the cover-binary link, release builds by `goreleaser-validate.yml` / `publish-dev.yml`. Fork PRs run the full secretless pipeline; merge-queue `merge_group` runs re-test the full suite against current main (the queue replaces the old require-up-to-date rule — never remove the `merge_group:` trigger, or queued PRs hang). CI logic lives in `scripts/ci/*.sh`, gated by `make lint-sh` (shellcheck) and `make lint-gha` (actionlint) inside `make verify` — not in inline YAML, except in the trusted-main deploy jobs where inline is the trust boundary.
- **Issue triage**: manual. `triage.yml` classified new/edited issues with GitHub Models until GitHub retired the service on 2026-07-30 — the endpoint now returns `410` unconditionally, so the workflow failed on every issue event and was removed, along with `.github/board-config.env` ([#431](https://github.com/Wave-RF/WaveHouse/issues/431)). The `PROJECT_BOARD_TOKEN` repo secret now has no consumers and **should be deleted, with its PAT revoked** — a branch cannot remove a repo secret, so this is a manual step. `area/*` / `security` / `breaking-change` labels and the board's `Priority` field are set by hand; maintainers on Claude Code can run the `/pm-triage` skill. PR labeling is untouched — `actions/labeler` in `housekeeping.yml` is path-based.
- **Code review** (advisory; the `main branch protection` ruleset is the actual merge gate — its `required_reviewers` rule requires an approval from the `@Wave-RF/wavehouse-admins` team, alongside the required `CI` status check): handled by external marketplace apps (CodeRabbit, Copilot) configured at the org/repo level, not by in-repo workflows. Inline findings post as review threads that `required_review_thread_resolution: true` blocks merge on until resolved.
- **Dependabot** (`.github/dependabot.yml`): opens weekly grouped PRs for Go modules, GitHub Actions, and the npm workspaces. They go through the same gate as any PR — an `@Wave-RF/wavehouse-admins` approval (`required_reviewers`) + the required checks — with **no auto-merge** (the former `dependabot-automerge.yml` was removed; auto-approve-and-merge is intentionally off, so every bump gets a human admin review).
Expand Down
Loading