ci: shard full-suite coverage and cache merge-base profile

The Coverage context was the PR critical path (~17 min end to end):
coverage-current re-ran the whole suite serially (-p 1, ~13 min) and
coverage-baseline re-ran it again at the merge-base (~13 min) although
that profile is a pure function of the base commit.

- coverage-current now owns only the scoped (standard-tier) profile;
  full-suite candidate profiles come from a 5-way shard matrix
  (app/cli/generators/helpers/remaining) that keeps -p 1 inside each
  shard on isolated runners. scripts/ci/test-packages.sh list-coverage
  defines the shards and verify proves the union equals the previous
  single-run package set exactly once.
- the aggregate Coverage job reassembles the disjoint shard profiles
  into coverage.txt before make coverage-gate, failing closed when a
  shard file is missing, so gate semantics (100% changed-code +
  scope-matched overall non-regression) are byte-compatible.
- coverage-baseline restores the merge-base full-suite profile from an
  exact-key cache (merge-base SHA + resolved Go version) written by the
  last green main push; any miss falls back to recomputing in the
  merge-base worktree. Exact key only - no prefix fallback, a near-miss
  profile would compare the candidate against the wrong commit.
- new contract tests pin the shard matrix, the assembly step, the
  exact-key cache pair, and the absence of restore-keys; the package
  plan test also covers the coverage shard partition.
This commit is contained in:
chichuan
2026-08-14 16:24:00 +08:00
parent 03258ca045
commit 3aa06e32fa
6 changed files with 390 additions and 41 deletions
+147 -38
View File
@@ -911,7 +911,7 @@ jobs:
coverage-current:
name: Coverage (current)
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' }}
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -926,10 +926,6 @@ jobs:
with:
go-version-file: go.mod
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Resolve authoritative coverage base
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -951,41 +947,33 @@ jobs:
- name: Build
run: make build
- name: Run current unit tests with coverage
- name: Run scoped unit tests with coverage
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
run: |
set -euo pipefail
if [ "$FULL_SUITE" = true ]; then
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
echo "No buildable Go package needs scoped coverage." \
>> "$GITHUB_STEP_SUMMARY"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 -p 1 \
-coverpkg="$coverpkg" \
-coverprofile=coverage.txt \
-covermode=atomic \
./ ./cmd/... ./internal/... ./skills/...
else
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
echo "No buildable Go package needs scoped coverage." \
>> "$GITHUB_STEP_SUMMARY"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 -p 1 \
-coverpkg="$coverpkg" \
-coverprofile=coverage.txt \
-covermode=atomic \
"${impacted_packages[@]}"
fi
"${impacted_packages[@]}"
fi
if [ "$(wc -l < coverage.txt)" -gt 1 ]; then
go tool cover -func=coverage.txt
@@ -998,6 +986,66 @@ jobs:
path: coverage.txt
retention-days: 1
coverage-current-full:
name: "Coverage (current: ${{ matrix.shard }})"
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard:
- app
- cli
- generators
- helpers
- remaining
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install archive tooling
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Build
run: make build
# Each shard keeps -p 1 so the authoritative measurement stays serial
# inside one instrumented process group; shards run on isolated runners,
# and scripts/ci/test-packages.sh verify proves the shard union equals
# the previous single full-suite package set exactly once.
- name: Run current shard tests with coverage
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
COVERAGE_SHARD: ${{ matrix.shard }}
run: |
set -euo pipefail
package_output="$(./scripts/ci/test-packages.sh list-coverage "$COVERAGE_SHARD")"
test -n "$package_output"
mapfile -t packages <<< "$package_output"
test "${#packages[@]}" -gt 0
go test -count=1 -p 1 \
-coverprofile="coverage-shard-$COVERAGE_SHARD.txt" \
-covermode=atomic \
"${packages[@]}"
go tool cover -func="coverage-shard-$COVERAGE_SHARD.txt" | tail -n 1
- name: Upload current shard coverage profile
uses: actions/upload-artifact@v4
with:
name: coverage-current-shard-${{ matrix.shard }}
path: coverage-shard-${{ matrix.shard }}.txt
retention-days: 1
coverage-supporting:
name: Coverage (supporting)
needs: lint
@@ -1051,14 +1099,11 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
id: setup-go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Resolve authoritative coverage base
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -1076,7 +1121,26 @@ jobs:
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV"
# The merge-base full-suite profile is a pure function of the base
# commit. Reuse the profile published by the last green push run of
# exactly that commit instead of re-running the whole suite; any key
# mismatch falls back to authoritative recomputation. Exact key only,
# never prefix fallback: a near-miss profile would compare the
# candidate against the wrong commit.
- name: Restore cached merge-base coverage profile
id: baseline-cache
if: needs.lint.outputs.full_suite == 'true'
uses: actions/cache/restore@v4
with:
path: coverage-base.txt
key: dws-coverage-full-v1-${{ env.COVERAGE_BASE_REF }}-go${{ steps.setup-go.outputs.go-version }}
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit != 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Run baseline unit tests with coverage
if: steps.baseline-cache.outputs.cache-hit != 'true'
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
@@ -1125,6 +1189,13 @@ jobs:
fi
)
- name: Save merge-base coverage profile cache
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: coverage-base.txt
key: dws-coverage-full-v1-${{ env.COVERAGE_BASE_REF }}-go${{ steps.setup-go.outputs.go-version }}
- name: Upload baseline coverage profile
uses: actions/upload-artifact@v4
with:
@@ -1137,6 +1208,7 @@ jobs:
needs:
- lint
- coverage-current
- coverage-current-full
- coverage-supporting
- coverage-baseline
- coverage-darwin
@@ -1152,6 +1224,7 @@ jobs:
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
PLATFORM_SENSITIVE: ${{ needs.lint.outputs.platform_sensitive }}
CURRENT_RESULT: ${{ needs.coverage-current.result }}
CURRENT_FULL_RESULT: ${{ needs.coverage-current-full.result }}
SUPPORTING_RESULT: ${{ needs.coverage-supporting.result }}
BASELINE_RESULT: ${{ needs.coverage-baseline.result }}
DARWIN_RESULT: ${{ needs.coverage-darwin.result }}
@@ -1159,6 +1232,7 @@ jobs:
run: |
failed=0
current_expected=success
current_full_expected=skipped
supporting_expected=skipped
baseline_expected=success
native_expected=skipped
@@ -1166,6 +1240,8 @@ jobs:
current_expected=skipped
baseline_expected=skipped
elif [ "$FULL_SUITE" = true ]; then
current_expected=skipped
current_full_expected=success
supporting_expected=success
fi
if [ "$CHANGELOG_ONLY" != true ] &&
@@ -1176,6 +1252,7 @@ jobs:
for profile in \
"current:$CURRENT_RESULT:$current_expected" \
"current shards:$CURRENT_FULL_RESULT:$current_full_expected" \
"supporting:$SUPPORTING_RESULT:$supporting_expected" \
"baseline:$BASELINE_RESULT:$baseline_expected"
do
@@ -1211,6 +1288,7 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
id: setup-go
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
uses: actions/setup-go@v5
with:
@@ -1234,11 +1312,12 @@ jobs:
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV"
- name: Download current coverage profile
- name: Download current coverage profiles
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
uses: actions/download-artifact@v4
with:
name: coverage-current-profile
pattern: coverage-current-*
merge-multiple: true
path: .
- name: Download supporting coverage profiles
@@ -1255,6 +1334,26 @@ jobs:
name: coverage-baseline-profile
path: .
# Shard profiles cover disjoint package sets, so their block-level
# concatenation is the same candidate profile one serial run produced.
# Every expected shard must be present; a missing shard would silently
# shrink the scope-matched overall comparison.
- name: Assemble full-suite coverage profile
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
shell: bash
run: |
set -euo pipefail
test ! -f coverage.txt
for shard in app cli generators helpers remaining; do
profile="coverage-shard-$shard.txt"
test -f "$profile"
test "$(head -n 1 "$profile")" = "mode: atomic"
done
printf 'mode: atomic\n' > coverage.txt
for shard in app cli generators helpers remaining; do
tail -n +2 "coverage-shard-$shard.txt" >> coverage.txt
done
- name: Enforce coverage gate
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
env:
@@ -1275,6 +1374,16 @@ jobs:
COVERAGE_ADDITIONAL_DIFF_PROFILE="$additional_profile" \
make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
# Publish this push's full-suite profile as the merge-base cache for
# future PRs whose merge-base is exactly this commit. Saved only after
# the gate passed so a broken run never becomes a baseline.
- name: Save push coverage profile as merge-base cache
if: github.event_name == 'push' && needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
uses: actions/cache/save@v4
with:
path: coverage.txt
key: dws-coverage-full-v1-${{ github.sha }}-go${{ steps.setup-go.outputs.go-version }}
- name: Generate coverage report
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
run: |