Skip to content

test(parity): compare with the reference's own functions and runs, and close what they found #266

test(parity): compare with the reference's own functions and runs, and close what they found

test(parity): compare with the reference's own functions and runs, and close what they found #266

Workflow file for this run

name: Go CLI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch: {}
schedule:
# Daily 06:00 UTC: the full runtime parity matrix + publish parity. On
# push/PR only the cases affected by the change run (see parity-runtime).
- cron: "0 6 * * *"
# Least privilege for every job in this workflow: the jobs only check out the repo.
# A job that needs more must widen it
# for itself. Without this default, GITHUB_TOKEN falls back to the repository's
# (possibly write) default — see the CodeQL "workflow does not contain permissions"
# rule, which TestWorkflowsDeclarePermissions pins.
permissions:
contents: read
# A newer push to a branch makes the run before it moot, so that run is cancelled; never on main,
# whose every commit needs its answer. 3 of 11 push runs (2026-10-23..25) ran for a commit a newer
# push had replaced.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
# The jobs that build and run containers land on spin-stack's docker8 fleet (spin-stack/ci: an
# ephemeral 8-vCPU EC2 instance per job, no nested virtualization). On GitHub's 4-vCPU runners a
# runtime parity shard took 10.7 min at p50 and 14.4 at p90, against 5.0 on Blacksmith's. A pull
# request from a fork runs on GitHub's runners instead: its code never reaches spin-stack's AWS
# account. Only a branch of this repository, which takes write access to push, runs on the fleet.
jobs:
# What a pull request changed, broken one small edit at a time (go-tools mutate): an
# edit no test refuses fails, so new code arrives with tests that hold it.
mutate:
if: github.event_name == 'pull_request'
runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'runs-on/fleet=docker8/env=production' || 'ubuntu-latest' }}
# 4 min on GitHub's runners for a diff's 200 mutations; a large diff's builds are many.
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- name: What this change broke, and whether a test noticed
env:
BASE: origin/${{ github.base_ref }}
run: go tool task mutate
lint-and-test:
runs-on: ubuntu-latest
# p90 3.2 min on GitHub's runners.
timeout-minutes: 15
permissions:
contents: read
outputs:
# Where the jobs that build and run containers land (see the top of this file).
fleet: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'runs-on/fleet=docker8/env=production' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- run: go tool task lint
- run: go tool task spec:compliance
- run: go tool task vuln
- run: go tool task test:race
- run: go tool task coverage
- name: Coverage summary
if: always()
run: go tool task coverage:summary SRC=coverage.out TITLE="unit (hermetic)"
- run: go tool task test:integration
- run: go tool task build
- run: go tool task build:smoke
# Binary covdata for the cross-lane merge (coverage-report job).
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always()
with:
name: covdata-unit
path: artifacts/coverage/data/unit
if-no-files-found: error
e2e:
runs-on: ${{ needs.lint-and-test.outputs.fleet }}
# p90 1.6 min on GitHub's runners.
timeout-minutes: 15
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- run: go tool task coverage:e2e
- name: Coverage summary
if: always()
run: go tool task coverage:summary SRC=artifacts/coverage/data/e2e TITLE=e2e
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always()
with:
name: covdata-e2e
path: artifacts/coverage/data/e2e
if-no-files-found: error
- if: always()
run: go tool task clean
# Hermetic parity gate: read-configuration golden comparison + the contract
# lane of the parity matrix (flag validation / output contract, no Docker).
parity:
# The contract and network lanes start no container: GitHub's runner is enough.
runs-on: ubuntu-latest
# p90 2.0 min on GitHub's runners.
timeout-minutes: 10
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
submodules: recursive
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
# pnpm installs the reference, through the firewall (task reference); pnpm 11 needs Node 22.13+.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 11.27.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: "24"
- run: go tool task reference
- run: go tool task parity:contract
- run: go tool task parity:network
# Coverage is measured separately with an instrumented Go binary. Keep the
# functional parity run above release-like, and avoid conflating subprocess
# coverage with the hermetic unit profile.
- run: go tool task coverage:parity-contract
- name: Coverage summary
if: always()
run: go tool task coverage:summary SRC=artifacts/coverage/data/contract TITLE=parity:contract
- if: always()
run: go tool task reference:commit
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always()
with:
name: parity-contract-network-v0.89.0
path: artifacts/
if-no-files-found: error
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always()
with:
name: covdata-contract
path: artifacts/coverage/data/contract
if-no-files-found: error
# "Did the repo change today?" gate for the expensive daily lanes. Manual dispatch
# always runs; a scheduled run only proceeds when HEAD has a commit within the last
# day (devtool daily-changed) — on a private/billed repo this skips the daily matrix
# on quiet days. Runs on schedule/dispatch so the daily lanes' `needs:` is satisfied.
daily-changes:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.check.outputs.run }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- id: check
run: go tool task ci:daily-changes
# Single sharded runtime parity matrix, parameterized by event:
# push/PR — SCOPED to the commands the diff can affect (devtool parity-affected),
# run with the plain binary: a fast gate (the full matrix is ~45 min).
# daily — the FULL matrix with the instrumented binary, so one run gates parity
# AND measures coverage; daily-changes keeps it off on quiet days.
# 2 shards across independent runners (each its own docker daemon) so the matrix
# never contends for one runner's CPU/disk/daemon. PARITY_SHARD_TOTAL MUST equal the
# length of matrix.shard — parity_sharding_workflow_test.go enforces it so a mismatch
# can't silently drop cases.
parity-runtime:
needs: [lint-and-test, daily-changes]
# Every push/PR, and on schedule/dispatch only when the repo changed. always() so a
# skipped daily-changes (push/PR) doesn't skip this job; the explicit lint-and-test
# check restores the normal "needs succeeded" gate.
if: >-
always()
&& needs.lint-and-test.result == 'success'
&& (github.event_name == 'push' || github.event_name == 'pull_request'
|| needs.daily-changes.outputs.run == 'true')
runs-on: ${{ needs.lint-and-test.outputs.fleet }}
# A shard took 14.4 min at p90 as one of four on GitHub's runners; one of two on them (a
# fork's pull request) takes about twice that.
timeout-minutes: 45
strategy:
# On a push or a pull request the first failing shard stops the other: what it would have
# found is in the next push's run. The daily run keeps going, since it also measures
# coverage.
fail-fast: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
matrix:
shard: [0, 1]
env:
PARITY_RUNTIME_TIMEOUT: "10m"
# Each shard owns half of the selected cases. -parallel 2 was the proven load on a
# 4-vCPU runner (per-case id-label / COMPOSE_PROJECT_NAME isolation); docker8's 8 vCPU
# hold twice that.
PARITY_PARALLEL: ${{ needs.lint-and-test.outputs.fleet == 'ubuntu-latest' && '2' || '4' }}
PARITY_SHARD_INDEX: ${{ matrix.shard }}
PARITY_SHARD_TOTAL: "2"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
submodules: recursive
# Full history so the push/PR diff base is present for the affected mapping.
fetch-depth: 0
# devtool (the affected mapping) runs before the composite setup, so Go here.
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- name: Plan this run
id: plan
# A pull request's diff is its base..head; a push's, what it moved the branch from..to.
env:
DIFF_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
DIFF_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
run: go tool task parity:plan
- if: steps.plan.outputs.run == 'true'
uses: ./.github/actions/setup-parity-runtime
# push/PR: scoped, plain binary — a fast gate.
- name: Run parity (scoped, plain)
if: steps.plan.outputs.run == 'true' && (github.event_name == 'push' || github.event_name == 'pull_request')
env:
PARITY_COMMAND: ${{ steps.plan.outputs.commands }}
run: go tool task parity:runtime
# daily: full matrix, instrumented binary — gates parity AND measures coverage.
- name: Run parity (full, instrumented)
if: steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
run: go tool task coverage:parity-runtime
- name: Coverage summary
if: always() && steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
run: go tool task coverage:summary SRC=artifacts/coverage/data/runtime TITLE="parity:runtime shard $PARITY_SHARD_INDEX"
- if: always() && steps.plan.outputs.run == 'true'
run: go tool task reference:commit
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always() && steps.plan.outputs.run == 'true'
with:
name: parity-runtime-v0.89.0-shard-${{ matrix.shard }}
path: artifacts/
if-no-files-found: error
# daily only: covdata slice for the cross-lane merge (distinct name per shard).
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always() && steps.plan.outputs.run == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
with:
name: covdata-runtime-shard-${{ matrix.shard }}
path: artifacts/coverage/data/runtime
if-no-files-found: error
- if: always() && steps.plan.outputs.run == 'true'
run: go tool task clean
# Publish parity (features/templates → ephemeral OCI registry). Daily only, its own
# job so it doesn't steal a runtime shard's runner.
parity-publish-full:
needs: [lint-and-test, daily-changes]
if: needs.daily-changes.outputs.run == 'true'
# Daily and dispatched only: this repository's own code.
runs-on: runs-on/fleet=docker8/env=production
# p90 3.3 min on GitHub's runners.
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
submodules: recursive
- uses: ./.github/actions/setup-parity-runtime
- run: go tool task coverage:parity-publish
- name: Coverage summary
if: always()
run: go tool task coverage:summary SRC=artifacts/coverage/data/publish TITLE=parity:publish
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: always()
with:
name: covdata-publish
path: artifacts/coverage/data/publish
if-no-files-found: error
- if: always()
run: go tool task clean
# Single gate + cross-lane coverage for ALL events (absorbs the old push/PR
# parity-runtime aggregator, the daily report, and the per-PR coverage-report):
# gates the runtime matrix (and, on daily, publish) into one pass/fail, and merges
# every lane's covdata (unit + e2e + contract, plus the daily runtime shards +
# publish) into the one true cross-lane number.
parity-runtime-gate:
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [lint-and-test, e2e, parity, parity-runtime, parity-publish-full]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- name: Download all lane covdata
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
with:
pattern: covdata-*
path: artifacts/coverage/data
- name: Normalize lane dirs
run: go tool task coverage:unpack
- name: Merged coverage report
run: go tool task coverage:merge
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: covdata-merged
path: artifacts/coverage/data/merged.out
if-no-files-found: warn
# Final verdict last, so coverage is posted even when a shard failed. A skipped
# lane (publish on push/PR, or the whole matrix on a quiet daily) counts as pass.
- name: Gate on runtime + publish
env:
RUNTIME_RESULT: ${{ needs.parity-runtime.result }}
PUBLISH_RESULT: ${{ needs.parity-publish-full.result }}
run: go tool task parity:gate
cross-compile:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: lint-and-test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
- run: go tool task build:cross