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
2 changes: 2 additions & 0 deletions .github/workflows/cache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ jobs:
mkdir -p web/dist && touch web/dist/.keep
go build ./...
go vet ./...
# make build compiles with production tags and -trimpath, which are separate cache entries.
CGO_ENABLED=0 go build -tags production -trimpath -o /dev/null ./cmd/sublane
- name: Save Go build cache
if: steps.build-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down
119 changes: 81 additions & 38 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: CI

# Merged commits were already tested on their pull request, and release tags rerun the full
# suite through workflow_call before anything is published.
# Merged commits were already tested on their pull request. A release tag whose source tree is
# byte-identical to a passing pull request tree reuses those results (see "Record the verified
# tree"); any other tag reruns the full suite through workflow_call before anything is published.
on:
pull_request:
workflow_call:
Expand All @@ -12,6 +13,10 @@ on:
release:
type: boolean
default: false
verified:
description: The source tree already passed every validation and test job on a pull request.
type: boolean
default: false

permissions:
contents: read
Expand All @@ -25,6 +30,7 @@ jobs:
validation:
# Independent checks run in parallel; every part shares the same pinned toolchain setup.
name: Validation (${{ matrix.part }})
if: ${{ !inputs.verified }}
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
Expand Down Expand Up @@ -102,13 +108,31 @@ jobs:
docker compose -f docker.compose.yaml -f docker.compose.build.yaml config --quiet

go-tests:
name: Go race (${{ matrix.group }})
name: Go race (${{ matrix.shard }})
if: ${{ !inputs.verified }}
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
group: [gateway-server-1, gateway-server-2, gateway-server-3, gateway-server-4, gateway-server-5, other-1, other-2, other-3, other-4]
# Gateway and server run in separate shards balanced by measured race-test time. Each
# package's last shard skips the earlier patterns, so new or renamed tests always run.
include:
- {shard: gateway-1, packages: ./internal/gateway, run: '^Test[AB]'}
- {shard: gateway-2, packages: ./internal/gateway, run: '^TestC'}
- {shard: gateway-3, packages: ./internal/gateway, run: '^Test[D-G]'}
- {shard: gateway-4, packages: ./internal/gateway, run: '^Test[H-N]'}
- {shard: gateway-5, packages: ./internal/gateway, run: '^Test[O-Q]'}
- {shard: gateway-6, packages: ./internal/gateway, run: '^TestR'}
- {shard: gateway-7, packages: ./internal/gateway, skip: '^Test[A-R]'}
- {shard: server-1, packages: ./internal/server, run: '^Test([AB]|[O-R])'}
- {shard: server-2, packages: ./internal/server, run: '^Test[C-G]'}
- {shard: server-3, packages: ./internal/server, run: '^Test[H-N]'}
- {shard: server-4, packages: ./internal/server, skip: '^Test[A-R]'}
- {shard: other-1, other: allocations}
- {shard: other-2, other: backup-auth-alerts}
- {shard: other-3, other: rest}
- {shard: other-4, other: accounts}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -117,10 +141,9 @@ jobs:
with:
go-version-file: go.mod
cache: false
# Race-instrumented objects differ from normal builds, so they keep their own cache. The first
# shard compiles the gateway and server with every dependency and saves for the others.
# Race-instrumented objects differ from normal builds, so they keep their own cache. The main
# branch warms it after each merge (cache.yaml); pull requests only restore it.
- name: Restore Go race build cache
id: go-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
Expand All @@ -133,40 +156,32 @@ jobs:
- name: Test with race detector
shell: bash
env:
TEST_GROUP: ${{ matrix.group }}
PACKAGES: ${{ matrix.packages }}
RUN: ${{ matrix.run }}
SKIP: ${{ matrix.skip }}
OTHER: ${{ matrix.other }}
run: |
# Shards partition by test name and package; each final shard is a complement so
# examples, fuzz seeds, future test names and new packages are always tested.
if [ -n "$PACKAGES" ]; then
args=()
if [ -n "$RUN" ]; then args+=(-run "$RUN"); fi
if [ -n "$SKIP" ]; then args+=(-skip "$SKIP"); fi
go test -race -timeout=20m "${args[@]}" "$PACKAGES"
exit
fi
# The rest shard is a complement, so new packages are always tested.
heavy_a='/internal/allocations$'
heavy_b='/internal/(backup|auth|alerts)$'
heavy_c='/internal/accounts$'
case "$TEST_GROUP" in
gateway-server-1) go test -race -timeout=20m -run '^Test[A-B]' ./internal/gateway ./internal/server ;;
gateway-server-2) go test -race -timeout=20m -run '^Test[C-G]' ./internal/gateway ./internal/server ;;
gateway-server-3) go test -race -timeout=20m -run '^Test[H-N]' ./internal/gateway ./internal/server ;;
gateway-server-4) go test -race -timeout=20m -run '^Test[O-R]' ./internal/gateway ./internal/server ;;
gateway-server-5) go test -race -timeout=20m -skip '^Test[A-R]' ./internal/gateway ./internal/server ;;
other-1|other-2|other-3|other-4)
all=$(go list ./... | grep -vE '/internal/(gateway|server)$')
case "$TEST_GROUP" in
other-1) package_list=$(grep -E "$heavy_a" <<< "$all") ;;
other-2) package_list=$(grep -E "$heavy_b" <<< "$all") ;;
other-3) package_list=$(grep -vE "$heavy_a|$heavy_b|$heavy_c" <<< "$all") ;;
other-4) package_list=$(grep -E "$heavy_c" <<< "$all") ;;
esac
mapfile -t packages <<< "$package_list"
go test -race -timeout=20m "${packages[@]}"
;;
*) echo "unknown test group: $TEST_GROUP" >&2; exit 1 ;;
all=$(go list ./... | grep -vE '/internal/(gateway|server)$')
case "$OTHER" in
allocations) package_list=$(grep -E "$heavy_a" <<< "$all") ;;
backup-auth-alerts) package_list=$(grep -E "$heavy_b" <<< "$all") ;;
accounts) package_list=$(grep -E "$heavy_c" <<< "$all") ;;
rest) package_list=$(grep -vE "$heavy_a|$heavy_b|$heavy_c" <<< "$all") ;;
*) echo "unknown package group: $OTHER" >&2; exit 1 ;;
esac
- name: Save Go race build cache
if: matrix.group == 'gateway-server-1' && steps.go-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ steps.go-cache.outputs.cache-primary-key }}
mapfile -t packages <<< "$package_list"
go test -race -timeout=20m "${packages[@]}"

containers:
name: Container (${{ matrix.arch }})
Expand Down Expand Up @@ -269,7 +284,35 @@ jobs:
VALIDATION_RESULT: ${{ needs.validation.result }}
GO_TESTS_RESULT: ${{ needs.go-tests.result }}
CONTAINERS_RESULT: ${{ needs.containers.result }}
VERIFIED: ${{ inputs.verified }}
run: |
test "$VALIDATION_RESULT" = success
test "$GO_TESTS_RESULT" = success
test "$CONTAINERS_RESULT" = success
if [ "$VERIFIED" = true ]; then
# Reused results: the release run builds and smoke-tests its own versioned images only.
test "$VALIDATION_RESULT" = skipped
test "$GO_TESTS_RESULT" = skipped
else
test "$VALIDATION_RESULT" = success
test "$GO_TESTS_RESULT" = success
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: github.event_name == 'pull_request'
with:
persist-credentials: false
# Every job in this run tested the same merge commit. Its tree hash names the artifact, so a
# release tag with an identical tree (same code, scripts and workflows) can reuse these results.
- name: Record the verified tree
id: tree
if: github.event_name == 'pull_request'
run: |
tree=$(git rev-parse 'HEAD^{tree}')
mkdir verified
printf '%s\n' "$tree" > verified/tree
echo "tree=$tree" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: github.event_name == 'pull_request'
with:
name: verified-tree-${{ steps.tree.outputs.tree }}
path: verified/tree
retention-days: 14
overwrite: true
27 changes: 27 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ jobs:
prepare:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
tag: ${{ steps.metadata.outputs.tag }}
verified: ${{ steps.reuse.outputs.verified }}
version: ${{ steps.metadata.outputs.version }}
prerelease: ${{ steps.metadata.outputs.prerelease }}
image: ${{ steps.metadata.outputs.image }}
Expand All @@ -30,13 +34,36 @@ jobs:
- name: Validate the version and registry path
id: metadata
run: node scripts/release.mjs metadata "$GITHUB_REF_NAME" "$GITHUB_REPOSITORY" >> "$GITHUB_OUTPUT"
# A tag whose tree is byte-identical to a pull request merge commit that passed every CI job
# in this repository needs no second test run; any other tag runs the full suite. Only
# same-repository pull request runs of ci.yaml count, so forks cannot vouch for a tree.
- name: Reuse tests from an identical verified tree
id: reuse
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY_ID: ${{ github.repository_id }}
run: |
tree=$(git rev-parse 'HEAD^{tree}')
verified=false
runs=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=verified-tree-$tree&per_page=20" \
--jq ".artifacts[] | select(.expired | not) | select(.workflow_run.head_repository_id == $REPOSITORY_ID) | .workflow_run.id")
for run in $runs; do
if gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run" \
--jq 'select(.conclusion == "success" and .event == "pull_request" and .path == ".github/workflows/ci.yaml") | .id' | grep -q .; then
verified=true
echo "Tree $tree passed pull request CI run $run; reusing its validation and tests."
break
fi
done
echo "verified=$verified" >> "$GITHUB_OUTPUT"

verify:
needs: prepare
uses: ./.github/workflows/ci.yaml
with:
version: ${{ needs.prepare.outputs.version }}
release: true
verified: ${{ needs.prepare.outputs.verified == 'true' }}

publish:
needs: [prepare, verify]
Expand Down
Loading