Merge protected Pulse main at batch frontier

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-29 14:02:16 +01:00
5 changed files with 219 additions and 10 deletions
+112 -9
View File
@@ -267,9 +267,9 @@ jobs:
strategy:
fail-fast: false
matrix:
# internal/api is by far the slowest package under -race (~10m), so it
# gets a dedicated shard; every other package splits across the rest.
shard: [api, rest-0, rest-1]
# internal/api has its own sharded job below; every other package
# splits across these two.
shard: [rest-0, rest-1]
steps:
- name: Report documentation-only change
@@ -304,15 +304,11 @@ jobs:
if: needs.changes.outputs.code == 'true'
env:
PULSE_DATA_DIR: /tmp/pulse-test-data
# internal/api alone takes ~22m under -race on a fast machine since
# the demo estate scaled to 50 nodes, and CI runners have needed
# roughly double the fast-machine time, hence the large per-binary
# budget. Remaining packages are split deterministically by list
# position.
# Packages other than internal/api are split deterministically by
# list position.
run: |
set -euo pipefail
case "${{ matrix.shard }}" in
api) pkgs="./internal/api" ;;
rest-0) pkgs=$(go list ./... | grep -v '/internal/api$' | awk 'NR % 2 == 0') ;;
rest-1) pkgs=$(go list ./... | grep -v '/internal/api$' | awk 'NR % 2 == 1') ;;
*) echo "unknown shard ${{ matrix.shard }}" >&2; exit 1 ;;
@@ -324,6 +320,113 @@ jobs:
printf 'Packages in this shard:\n%s\n' "$pkgs"
go test -race -timeout 50m $pkgs
# internal/api alone took ~27 of the ~31 minutes of the old single
# `Backend tests (api)` job under -race, which made it the critical path of
# every pull request. Its top-level tests are split across these shards
# as contiguous runs of the package's own test order, and the
# `Backend tests (api)` job below keeps the required check name by passing
# only when every shard passed.
backend-api:
name: Backend tests (api-${{ matrix.index }})
needs: changes
# Like the rest shards, expand even for documentation-only changes so the
# verdict job sees real shard results rather than skipped jobs.
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
# Keep API_SHARD_COUNT equal to the number of indexes listed here.
index: [0, 1, 2, 3]
env:
API_SHARD_COUNT: 4
steps:
- name: Report documentation-only change
if: needs.changes.outputs.code != 'true'
run: echo "No backend code changed. Tests are not required for this shard."
- name: Checkout repository
if: needs.changes.outputs.code == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
if: needs.changes.outputs.code == 'true'
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- name: Provide frontend embed stub
if: needs.changes.outputs.code == 'true'
run: |
mkdir -p internal/api/frontend-modern/dist
[ -f internal/api/frontend-modern/dist/index.html ] || \
printf '<!doctype html><title>ci embed stub</title>\n' > internal/api/frontend-modern/dist/index.html
- name: Go unit tests (internal/api shard ${{ matrix.index }})
if: needs.changes.outputs.code == 'true'
env:
PULSE_DATA_DIR: /tmp/pulse-test-data
API_SHARD_INDEX: ${{ matrix.index }}
# The test list is computed from the checked-out commit at run time,
# so every shard sees the same list and a test added later lands in
# exactly one shard with no workflow edit. The list keeps go test's
# own order (file name, then declaration), which is also run order,
# and each shard takes one contiguous slice of it. Some internal/api
# tests still lean on package state left by the tests just before
# them (for example admin bypass or session store globals), and an
# interleaved split by name broke dozens of them, while contiguous
# slices keep those neighbours together. Listing with -race builds
# the same test binary the run then reuses from cache. Benchmarks
# are left out because plain go test never runs them.
run: |
set -euo pipefail
listing=$(go test -race -list . ./internal/api)
tests=$(printf '%s\n' "$listing" | grep -E '^(Test|Fuzz|Example)[^[:space:]]*$' || true)
if [ -z "$tests" ]; then
echo "::error::go test -list found no tests in ./internal/api"
exit 1
fi
total=$(printf '%s\n' "$tests" | wc -l | tr -d ' ')
selected=$(printf '%s\n' "$tests" | awk -v n="$API_SHARD_COUNT" -v i="$API_SHARD_INDEX" -v total="$total" \
'NR > int(total * i / n) && NR <= int(total * (i + 1) / n)')
if [ -z "$selected" ]; then
echo "::error::internal/api shard ${API_SHARD_INDEX} of ${API_SHARD_COUNT} resolved to an empty test list"
exit 1
fi
pattern="^($(printf '%s\n' "$selected" | paste -sd '|' -))\$"
# Linux rejects a single argument over 128 KiB. Fail with a clear
# instruction instead of an opaque exec error.
if [ "${#pattern}" -gt 120000 ]; then
echo "::error::internal/api shard pattern is ${#pattern} bytes; add a shard index and raise API_SHARD_COUNT"
exit 1
fi
echo "internal/api shard ${API_SHARD_INDEX} of ${API_SHARD_COUNT} runs $(printf '%s\n' "$selected" | wc -l | tr -d ' ') of ${total} tests"
go test -race -timeout 50m -run "$pattern" ./internal/api
# Required check name. Branch protection matches `Backend tests (api)`
# exactly, so this job must keep that name and must fail unless every
# internal/api shard succeeded (a skipped or cancelled shard fails it too).
backend-api-verdict:
name: Backend tests (api)
needs: backend-api
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every internal/api shard
env:
API_SHARDS_RESULT: ${{ needs.backend-api.result }}
run: |
if [ "$API_SHARDS_RESULT" != success ]; then
echo "::error::internal/api shards did not all pass (result: ${API_SHARDS_RESULT})"
exit 1
fi
echo "Every internal/api shard passed."
scripts-and-build:
name: Script smoke tests & backend build
needs: changes
+4 -1
View File
@@ -203,9 +203,12 @@ jobs:
# Runs even when the stable tier failed (its data still counts toward
# promotion), but not when the stable tier was skipped — that means the
# environment never came up and every probation spec would fail on it.
# Pull requests skip it. It cannot gate a PR, and the promotion rule in
# e2e-tiering.mjs counts only main runs, so on a PR it only held each
# of the eight shard runners for about eight more minutes.
- name: Run probation-tier E2E suite (non-gating)
id: probation
if: ${{ !cancelled() && steps.stable.conclusion != 'skipped' }}
if: ${{ !cancelled() && github.event_name != 'pull_request' && steps.stable.conclusion != 'skipped' }}
continue-on-error: true
# A broken probation surface can otherwise consume the entire 60m job
# budget through retries and cancel the job before continue-on-error