From a18992bd5d1879cd5e3f3a7b07f3dbd81deafbcc Mon Sep 17 00:00:00 2001 From: Peli de Halleux <4175913+pelikhan@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:56:05 -0800 Subject: [PATCH] Speed up Go CI and report daily validation failures Shard core packages across four jobs and reserve the 16 slowest core scenarios for daily and manual runs. File or update a failure issue from daily full validation while preserving complete scheduled coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/cgo.yml | 13 +++- .github/workflows/ci-daily.yml | 5 +- CONTRIBUTING.md | 15 +++- scripts/ci-report.mjs | 17 ++++ scripts/ci-test-plan.mjs | 4 +- server/cmd/cao-dashboard/adx_setup_test.go | 3 + .../cao-dashboard/benchmark_queries_test.go | 6 ++ server/cmd/cao-dashboard/main_test.go | 3 + .../adxingest/postgres_integration_test.go | 3 + .../audit_engine_postgres_test.go | 3 + .../dashboardstore/conformance_test.go | 3 + .../ingest/audit_curation_integration_test.go | 12 +++ server/internal/ingest/ingest_test.go | 3 + .../ingest/reclamation_integration_test.go | 3 + .../ingest/recovery_integration_test.go | 3 + .../internal/simulator/dashboard_read_test.go | 3 + server/internal/webhookgateway/stress_test.go | 3 + tests/unit/cao-ci-tiers.test.mjs | 78 ++++++++++++++++++- tests/unit/cao-go-ci-plan.test.mjs | 60 ++++++++++++-- .../workflow-contract-ci-triggers.test.mjs | 12 +-- 20 files changed, 231 insertions(+), 21 deletions(-) diff --git a/.github/workflows/cgo.yml b/.github/workflows/cgo.yml index 92e1225865..d43679d69d 100644 --- a/.github/workflows/cgo.yml +++ b/.github/workflows/cgo.yml @@ -257,7 +257,7 @@ jobs: GO_TEST_AREA: ${{ matrix.area }} TEST_SHARD: ${{ matrix.shard }} TEST_SHARD_COUNT: ${{ matrix.shards }} - GO_TEST_MODE: ${{ github.event_name == 'pull_request' && '-short' || '-count=1' }} + GO_TEST_MODE: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '-count=1' || '-short' }} POSTGRES_URL: postgresql://postgres@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/cao_server_test?sslmode=disable CAO_TEST_POSTGRES_URL: postgresql://postgres@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/cao_server_test?sslmode=disable run: | @@ -268,12 +268,16 @@ jobs: storage) packages=(./internal/postgresx) ;; core) all_packages="$(go -C server list ./...)" - mapfile -t packages < <(printf '%s\n' "$all_packages" | grep -vE '/internal/(server|hostedcli|postgresx)$') + packages=() + while IFS= read -r package; do packages+=("$package"); done < <(printf '%s\n' "$all_packages" | + grep -vE '/internal/(server|hostedcli|postgresx)$' | + LC_ALL=C sort | + awk -v shard="$TEST_SHARD" -v count="$TEST_SHARD_COUNT" 'NR % count == shard') ;; *) echo "Unknown Go test area: $GO_TEST_AREA" >&2; exit 1 ;; esac test "${#packages[@]}" -gt 0 - if (( TEST_SHARD_COUNT > 1 )); then + if [[ "$GO_TEST_AREA" != core ]] && (( TEST_SHARD_COUNT > 1 )); then pattern="$(go -C server test -p 1 "$GO_TEST_MODE" -list '^(Test|Example|Fuzz)' "${packages[@]}" | LC_ALL=C sort -u | awk -v shard="$TEST_SHARD" -v count="$TEST_SHARD_COUNT" ' @@ -620,6 +624,7 @@ jobs: env: TEST_SHARD: ${{ matrix.shard }} TEST_SHARD_COUNT: ${{ strategy.job-total }} + GO_TEST_MODE: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '-count=1' || '-short' }} run: | set -euo pipefail pattern=$(go -C server test -p 1 -list '^(Test|Example|Fuzz)' ./... | @@ -631,7 +636,7 @@ jobs: } }') test -n "$pattern" - go -C server test -p 1 -run "^($pattern)$" ./... + go -C server test -p 1 -run "^($pattern)$" ./... "$GO_TEST_MODE" - name: Build dashboard and server if: matrix.shard == 0 run: | diff --git a/.github/workflows/ci-daily.yml b/.github/workflows/ci-daily.yml index bbc4622f74..0cd37aab9c 100644 --- a/.github/workflows/ci-daily.yml +++ b/.github/workflows/ci-daily.yml @@ -163,6 +163,7 @@ jobs: permissions: contents: read actions: read + issues: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -171,12 +172,13 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: SUITES: ${{ toJSON(needs) }} + REPORT_FAILURE: ${{ github.ref == 'refs/heads/main' && needs.revision.result == 'success' }} with: script: | const { writeFileSync, appendFileSync } = require('node:fs') const { pathToFileURL } = require('node:url') const { resolve } = require('node:path') - const { buildCIReport } = await import(pathToFileURL(resolve('scripts/ci-report.mjs')).href) + const { buildCIReport, reportCIFailure } = await import(pathToFileURL(resolve('scripts/ci-report.mjs')).href) const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { ...context.repo, run_id: context.runId, filter: 'latest', per_page: 100, }) @@ -186,6 +188,7 @@ jobs: writeFileSync('ci-daily-report.json', JSON.stringify(report, null, 2) + '\n') writeFileSync('ci-daily-report.md', report.markdown) appendFileSync(process.env.GITHUB_STEP_SUMMARY, report.markdown) + if (process.env.REPORT_FAILURE === 'true') await reportCIFailure(github, context.repo, report) if (report.status !== 'success') core.setFailed('Daily validation failed or is incomplete; see the suite report.') - name: Retain daily report if: always() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2446d16b2e..976a63a8ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,12 +136,19 @@ and authenticated compatibility tests, bootstrap, and service-backed integration run after merge rather than on the PR critical path. A green PR is intentionally not a claim that every integration or performance scenario passed. -Main pushes select full suites through each workflow's path filters. Root +Main pushes select suites through each workflow's path filters. Root contract areas are further selected by `scripts/ci-test-plan.mjs`; documentation-only changes omit root integration/load/API checks. Unknown paths or an unavailable comparison base widen selection. Fast contract checks run concurrently with units; the existing `test` aggregate check requires both to succeed. +Go core packages run in four disjoint shards; storage retains its four +test-name shards. PRs and main pushes use `-short`, including the service-backed +dashboard Go suite. The 16 core tests measured at 10 seconds or more in the +investigated CI run (cross-module CLI build, fleet simulation, PostgreSQL +ingestion/publication, read conformance, and query benchmarks) skip in short +mode. Daily and manual Go runs omit `-short` and retain these scenarios. + `Daily full validation` (`.github/workflows/ci-daily.yml`) runs at 06:37 UTC and can be dispatched manually on main. It reuses the complete contract, dashboard, Go, bootstrap, simulator, Azure, query/MCP parity, mobile, view, @@ -155,7 +162,11 @@ The daily run publishes an Actions summary and a 30-day `ci-daily-report` artifact (Markdown and JSON), including every suite's outcome, total completed runner-minutes, and the 20 slowest jobs. Per-suite traces, profiles, and detailed reports remain attached to the same run. Failed, cancelled, or skipped suites -make the aggregate report incomplete. Campaign lifecycle API-quota exhaustion +make the aggregate report incomplete. A failed or incomplete daily run on main +opens a `Daily full validation failed` issue, or updates the existing open bot +issue with the latest failing revision, suite outcomes, and job links. Successful +runs do not open issues or automatically close an investigation. +Campaign lifecycle API-quota exhaustion also fails daily/manual full runs; main pushes retain infrastructure-incomplete warnings instead of blocking on shared quota. These measurements exclude queue time and billing multipliers. Use them to diff --git a/scripts/ci-report.mjs b/scripts/ci-report.mjs index 8a1140d6da..a6cee2c1fc 100644 --- a/scripts/ci-report.mjs +++ b/scripts/ci-report.mjs @@ -36,3 +36,20 @@ export function buildCIReport(suites, jobs, url, sha) { ].join("\n"); return { status, sha, url, suites: results, runnerMinutes, jobs: timedJobs, markdown }; } + +export async function reportCIFailure(github, repo, report) { + if (report.status === "success") return; + const marker = ""; + const title = "Daily full validation failed"; + const body = `${marker}\n\n${report.markdown}`; + const issues = await github.paginate(github.rest.issues.listForRepo, { + ...repo, state: "open", creator: "github-actions[bot]", per_page: 100, + }); + const existing = issues.find((issue) => + !issue.pull_request && issue.body?.includes(marker)); + if (existing) { + await github.rest.issues.update({ ...repo, issue_number: existing.number, body }); + } else { + await github.rest.issues.create({ ...repo, title, body }); + } +} diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 1b9adb54f7..7a2df89def 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -45,8 +45,8 @@ export function goTestPlan(files, full = false) { return { matrix: { include: goTestAreas.filter((area) => selected.has(area)).flatMap((area) => - Array.from({ length: area === "storage" ? 4 : 1 }, (_, shard) => ({ - area, shard, shards: area === "storage" ? 4 : 1, + Array.from({ length: ["storage", "core"].includes(area) ? 4 : 1 }, (_, shard) => ({ + area, shard, shards: ["storage", "core"].includes(area) ? 4 : 1, }))), }, tests: selected.size > 0, diff --git a/server/cmd/cao-dashboard/adx_setup_test.go b/server/cmd/cao-dashboard/adx_setup_test.go index 78cef713a1..e7d0695105 100644 --- a/server/cmd/cao-dashboard/adx_setup_test.go +++ b/server/cmd/cao-dashboard/adx_setup_test.go @@ -13,6 +13,9 @@ import ( ) func TestADXSetupSettingsMatchRuntime(t *testing.T) { + if testing.Short() { + t.Skip("slow cross-module CLI build; covered by daily full validation") + } // The extension and server are separate modules; exercise the actual CLI // output through the runtime parsers instead of duplicating their contract. binary := filepath.Join(t.TempDir(), "gh-cao") diff --git a/server/cmd/cao-dashboard/benchmark_queries_test.go b/server/cmd/cao-dashboard/benchmark_queries_test.go index 1205b441bd..c2fe826adf 100644 --- a/server/cmd/cao-dashboard/benchmark_queries_test.go +++ b/server/cmd/cao-dashboard/benchmark_queries_test.go @@ -236,6 +236,9 @@ func TestBuildBenchmarkMeasurementReturnsMeasurement(t *testing.T) { } func TestBenchmarkQueriesWithDeployedSubset(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL benchmark; covered by daily full validation") + } url := os.Getenv("POSTGRES_URL") if url == "" { t.Skip("POSTGRES_URL is not set") @@ -299,6 +302,9 @@ func TestBenchmarkQueriesWithDeployedSubset(t *testing.T) { } func TestBenchmarkCleanupPreservesOtherNamespaces(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL benchmark; covered by daily full validation") + } url := os.Getenv("POSTGRES_URL") if url == "" { t.Skip("POSTGRES_URL is not set") diff --git a/server/cmd/cao-dashboard/main_test.go b/server/cmd/cao-dashboard/main_test.go index d2fd1d798b..99b5e83873 100644 --- a/server/cmd/cao-dashboard/main_test.go +++ b/server/cmd/cao-dashboard/main_test.go @@ -26,6 +26,9 @@ import ( ) func TestServeWithExistingPostgresOperationalTables(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL server integration; covered by daily full validation") + } endpoint := os.Getenv("POSTGRES_URL") if endpoint == "" { t.Skip("POSTGRES_URL is not set") diff --git a/server/internal/adxingest/postgres_integration_test.go b/server/internal/adxingest/postgres_integration_test.go index cb97b99eca..5f322184f2 100644 --- a/server/internal/adxingest/postgres_integration_test.go +++ b/server/internal/adxingest/postgres_integration_test.go @@ -53,6 +53,9 @@ func publishSession(t *testing.T, data *postgresx.Store, tokens int) { } func TestPostgresSimulatorDurableRestartAndCanonicalReplacement(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } data := testutil.Postgres(t, t.Context(), "POSTGRES_URL") publishSession(t, data, 0) endpoint := os.Getenv("POSTGRES_URL") diff --git a/server/internal/collectnative/audit_engine_postgres_test.go b/server/internal/collectnative/audit_engine_postgres_test.go index 665af90197..578743c63f 100644 --- a/server/internal/collectnative/audit_engine_postgres_test.go +++ b/server/internal/collectnative/audit_engine_postgres_test.go @@ -25,6 +25,9 @@ func (engineAuditProvider) Jobs(_ context.Context, run collectnative.Run, visit } func TestNativeLegacyEngineAndGatewayPostgresPublication(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL publication integration; covered by daily full validation") + } for _, test := range []struct{ engine, content string }{ {"codex", "thinking\ntool github.read({})\ntokens used\n60,001"}, {"claude", `prefix {'usage':{'input_tokens':60000,'output_tokens':1},'cost':0.25}`}, diff --git a/server/internal/dashboardstore/conformance_test.go b/server/internal/dashboardstore/conformance_test.go index e447a412e3..79a3f96ee2 100644 --- a/server/internal/dashboardstore/conformance_test.go +++ b/server/internal/dashboardstore/conformance_test.go @@ -22,6 +22,9 @@ type readFixture struct { } func TestReadContract(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL read conformance; covered by daily full validation") + } repositories := []model.Row{ {"id": "repo:1", "owner": "octo", "name": "one"}, {"id": "repo:2", "owner": "octo", "name": "two"}, diff --git a/server/internal/ingest/audit_curation_integration_test.go b/server/internal/ingest/audit_curation_integration_test.go index c0192d683f..cf566736ba 100644 --- a/server/internal/ingest/audit_curation_integration_test.go +++ b/server/internal/ingest/audit_curation_integration_test.go @@ -19,6 +19,9 @@ import ( ) func TestUnchangedArtifactRevisionCuratesExistingAudits(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store, config := ingestTestStoreConfig(t) options := Options{DatabaseQueriesPath: "../../../dashboard/site/src/data/queries/database.json"} directory, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") @@ -47,6 +50,9 @@ func TestUnchangedArtifactRevisionCuratesExistingAudits(t *testing.T) { } func TestAuditCurationValidatesEveryTransportedRecord(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store := ingestTestStore(t) directory := scratchDirectory(t) recent, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") @@ -111,6 +117,9 @@ func TestAuditCurationValidatesEveryTransportedRecord(t *testing.T) { } func TestUnsupportedAuditEvidencePreservesPublishedIngestion(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store := ingestTestStore(t) directory, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") first, err := Run(ctx, store, directory, Options{DatabaseQueriesPath: "../../../dashboard/site/src/data/queries/database.json"}) @@ -134,6 +143,9 @@ func TestUnsupportedAuditEvidencePreservesPublishedIngestion(t *testing.T) { } func TestAuditCurationDoesNotHideTransportedOrphan(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store := ingestTestStore(t) options := Options{DatabaseQueriesPath: "../../../dashboard/site/src/data/queries/database.json"} recent, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") diff --git a/server/internal/ingest/ingest_test.go b/server/internal/ingest/ingest_test.go index 04eaddb729..51b1980f64 100644 --- a/server/internal/ingest/ingest_test.go +++ b/server/internal/ingest/ingest_test.go @@ -62,6 +62,9 @@ func TestValidateManifestFailsClosed(t *testing.T) { } func TestLocalIngestionReceiptsRejectForeignTransactions(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } for _, row := range []model.Row{ {"id": "unrelated", "kind": "local-dashboard-ingestion"}, {"id": "local-dashboard-ingestion", "kind": "unrelated"}, diff --git a/server/internal/ingest/reclamation_integration_test.go b/server/internal/ingest/reclamation_integration_test.go index 6d0c3ce6ec..91363ba04f 100644 --- a/server/internal/ingest/reclamation_integration_test.go +++ b/server/internal/ingest/reclamation_integration_test.go @@ -57,6 +57,9 @@ func ingestTestStoreConfig(t *testing.T) (context.Context, *postgresx.Store, *pg } func TestFreshNativeIngestionReuseForceAndQueries(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store := ingestTestStore(t) options := Options{DatabaseQueriesPath: "../../../dashboard/site/src/data/queries/database.json"} directory, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") diff --git a/server/internal/ingest/recovery_integration_test.go b/server/internal/ingest/recovery_integration_test.go index bb28e0bac7..87aa7678a2 100644 --- a/server/internal/ingest/recovery_integration_test.go +++ b/server/internal/ingest/recovery_integration_test.go @@ -13,6 +13,9 @@ import ( ) func TestEmptyPostgresRebuildAndFailedIngestionPreservesCurrentData(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL ingestion integration; covered by daily full validation") + } ctx, store := ingestTestStore(t) directory, _ := testutil.RecentSnapshot(t, "../../testdata/deployed-subset") options := Options{DatabaseQueriesPath: "../../../dashboard/site/src/data/queries/database.json"} diff --git a/server/internal/simulator/dashboard_read_test.go b/server/internal/simulator/dashboard_read_test.go index 7a4a416a05..22f2b6a387 100644 --- a/server/internal/simulator/dashboard_read_test.go +++ b/server/internal/simulator/dashboard_read_test.go @@ -108,6 +108,9 @@ func newDashboardReadFixture(t *testing.T) dashboardReadFixture { } func TestDashboardReadSyntheticScenario(t *testing.T) { + if testing.Short() { + t.Skip("slow PostgreSQL dashboard scenario; covered by daily full validation") + } fixture := newDashboardReadFixture(t) app, err := server.New(t.Context(), fixture.services, fixture.config) if err != nil { diff --git a/server/internal/webhookgateway/stress_test.go b/server/internal/webhookgateway/stress_test.go index eccca5067d..d510b541f2 100644 --- a/server/internal/webhookgateway/stress_test.go +++ b/server/internal/webhookgateway/stress_test.go @@ -314,6 +314,9 @@ func runFleetStress(t *testing.T, repositories, developers, days int) { } func TestGitHubSimulatorFleetSmoke(t *testing.T) { + if testing.Short() { + t.Skip("slow fleet simulation; covered by daily full validation") + } runFleetStress(t, 10, 10, 2) } diff --git a/tests/unit/cao-ci-tiers.test.mjs b/tests/unit/cao-ci-tiers.test.mjs index e52df76fb4..a4be86cf50 100644 --- a/tests/unit/cao-ci-tiers.test.mjs +++ b/tests/unit/cao-ci-tiers.test.mjs @@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process"; import test from "node:test"; import { parse } from "yaml"; import { changedFiles, testPlan } from "../../scripts/ci-test-plan.mjs"; -import { buildCIReport } from "../../scripts/ci-report.mjs"; +import { buildCIReport, reportCIFailure } from "../../scripts/ci-report.mjs"; import { unitTestAreas } from "../../scripts/run-unit-area.mjs"; const load = (name) => parse(readFileSync(`.github/workflows/${name}`, "utf8")); @@ -61,6 +61,13 @@ test("daily calls every suite at the same revision and reports even on failure", assert.equal(job.secrets, undefined, "test suites do not inherit deployment secrets"); } assert.equal(daily.jobs.report.if, "always()"); + assert.equal(daily.jobs.report.permissions.issues, "write"); + assert.equal(daily.permissions.issues, undefined); + const publish = daily.jobs.report.steps.find((step) => step.name === "Publish suite outcomes and slowest jobs"); + assert.equal(publish.env.REPORT_FAILURE, + "${{ github.ref == 'refs/heads/main' && needs.revision.result == 'success' }}"); + assert.match(publish.with.script, /if \(process\.env\.REPORT_FAILURE === 'true'\) await reportCIFailure/); + assert.match(publish.with.script, /if \(report\.status !== 'success'\) core\.setFailed/); assert.deepEqual(daily.jobs.deployed.with, { full: true }); assert.deepEqual(daily.jobs["company-stress"].with, { preset: "large" }); assert.equal(daily.jobs["worker-memory"].needs, "revision"); @@ -117,3 +124,72 @@ test("daily reporting counts runner time, ranks slow jobs, and rejects incomplet assert.equal(buildCIReport({ suite: { result: "success" } }, jobs, "", "").status, "incomplete"); assert.equal(buildCIReport({ suite: { result: "success" } }, [jobs[0]], "", "").status, "success"); }); + +test("daily validation opens one failure issue and updates it with the latest evidence", async () => { + const repo = { owner: "octo", repo: "control" }; + const calls = []; + const open = [{ number: 1, title: "Daily full validation failed", body: "unrelated" }]; + const github = { + rest: { + issues: { + listForRepo: Symbol("list"), + create: async (request) => { + calls.push(["create", request]); + open.push({ number: 2, ...request }); + }, + update: async (request) => calls.push(["update", request]), + }, + }, + paginate: async (method, request) => { + assert.equal(method, github.rest.issues.listForRepo); + assert.deepEqual(request, { ...repo, state: "open", creator: "github-actions[bot]", per_page: 100 }); + return open; + }, + }; + const report = buildCIReport({ server: { result: "failure" } }, [], "https://example.com/run", "first-sha"); + await reportCIFailure(github, repo, report); + assert.equal(calls.length, 1); + assert.equal(calls[0][0], "create"); + assert.equal(calls[0][1].title, "Daily full validation failed"); + assert.ok(calls[0][1].body.includes(report.markdown)); + const latest = buildCIReport({ server: { result: "cancelled" } }, [], "https://example.com/retry", "next-sha"); + await reportCIFailure(github, repo, latest); + assert.deepEqual(calls[1], ["update", { + ...repo, issue_number: 2, body: `\n\n${latest.markdown}`, + }]); + // A copied marker in a PR must not turn a pull request into the failure tracker. + open[1].pull_request = {}; + await reportCIFailure(github, repo, latest); + assert.equal(calls[2][0], "create"); +}); + +test("successful daily validation does not call the issue API", async () => { + const job = { + name: "server", status: "completed", conclusion: "success", + started_at: "2026-01-01T00:00:00Z", completed_at: "2026-01-01T00:00:30Z", + }; + const report = buildCIReport({ server: { result: "success" } }, [job], "", ""); + assert.equal(report.status, "success"); + await reportCIFailure({}, {}, report); +}); + +test("daily failure reporting propagates issue API errors", async () => { + const error = new Error("issue API unavailable"); + const report = buildCIReport({ server: { result: "failure" } }, [], "", ""); + for (const operation of ["list", "create", "update"]) { + const github = { + rest: { + issues: { + listForRepo: Symbol("list"), + create: async () => { throw error; }, + update: async () => { throw error; }, + }, + }, + paginate: async () => { + if (operation === "list") throw error; + return operation === "create" ? [] : [{ number: 2, body: "" }]; + }, + }; + await assert.rejects(reportCIFailure(github, {}, report), error); + } +}); diff --git a/tests/unit/cao-go-ci-plan.test.mjs b/tests/unit/cao-go-ci-plan.test.mjs index d78699c02a..41da1ac389 100644 --- a/tests/unit/cao-go-ci-plan.test.mjs +++ b/tests/unit/cao-go-ci-plan.test.mjs @@ -84,7 +84,8 @@ test("Go unit jobs partition every package exactly once, including future packag const prefix = "github.com/githubnext/gh-cao/server/"; const step = jobs["server-tests"].steps.find((step) => step.name === "Unit tests"); const covered = []; - for (const area of goTestAreas) { + const matrix = goTestPlan([], true).matrix.include.filter(({ area, shard }) => area !== "storage" || shard === 0); + for (const { area, shard, shards } of matrix) { const result = spawnSync("bash", ["-c", ` go() { if [[ "$3" == "list" ]]; then printf '%s\\n' "$MOCK_PACKAGES"; @@ -95,7 +96,8 @@ test("Go unit jobs partition every package exactly once, including future packag encoding: "utf8", env: { ...process.env, MOCK_PACKAGES: packages.map((name) => prefix + name).join("\n"), - GO_TEST_AREA: area, GO_TEST_MODE: "-short", TEST_SHARD: "0", TEST_SHARD_COUNT: "1", + GO_TEST_AREA: area, GO_TEST_MODE: "-short", TEST_SHARD: String(shard), + TEST_SHARD_COUNT: area === "core" ? String(shards) : "1", }, }); assert.equal(result.status, 0, result.stderr); @@ -107,7 +109,8 @@ test("Go unit jobs partition every package exactly once, including future packag assert.deepEqual(covered.sort(), packages.sort()); assert.match(step.env.POSTGRES_URL, /job\.services\.postgres/); assert.match(step.env.CAO_TEST_POSTGRES_URL, /job\.services\.postgres/); - assert.match(step.env.GO_TEST_MODE, /'-short' \|\| '-count=1'/); + assert.equal(step.env.GO_TEST_MODE, + "${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '-count=1' || '-short' }}"); }); test("Go unit selection propagates package-list and test failures", () => { @@ -124,19 +127,64 @@ test("Go unit selection propagates package-list and test failures", () => { } }); -test("storage-only changes select four shards without duplicating other areas", () => { +test("core and storage select four shards while other areas retain a single job", () => { const shards = [0, 1, 2, 3].map((shard) => ({ area: "storage", shard, shards: 4 })); assert.deepEqual(goTestPlan(["server/internal/postgresx/schema_test.go"]).matrix.include, shards); const matrix = goTestPlan([], true).matrix.include; assert.deepEqual(matrix.filter(({ area }) => area === "storage"), shards); - assert.deepEqual(matrix.filter(({ area }) => area !== "storage"), - ["http", "hosted-cli", "core"].map((area) => ({ area, shard: 0, shards: 1 }))); + const core = shards.map((shard) => ({ ...shard, area: "core" })); + assert.deepEqual(goTestPlan(["server/internal/query/query_test.go"]).matrix.include, core); + assert.deepEqual(matrix.filter(({ area }) => area === "core"), core); + assert.deepEqual(matrix.filter(({ area }) => !["storage", "core"].includes(area)), + ["http", "hosted-cli"].map((area) => ({ area, shard: 0, shards: 1 }))); const step = jobs["server-tests"].steps.find((step) => step.name === "Unit tests"); assert.equal(step.env.TEST_SHARD, "${{ matrix.shard }}"); assert.equal(step.env.TEST_SHARD_COUNT, "${{ matrix.shards }}"); assert.ok(jobs["server-tests"].services.postgres); }); +test("core package shards are deterministic, avoid test-name discovery, and preserve test modes", () => { + const step = jobs["server-tests"].steps.find((step) => step.name === "Unit tests"); + const packages = Array.from({ length: 11 }, (_, i) => `example/internal/package${i}`); + const mock = ` + go() { + if [[ "$3" == "list" ]]; then printf '%s\\n' "$MOCK_PACKAGES"; + else + [[ "$*" != *" -list "* ]] || return 42 + [[ "$6" == "$GO_TEST_MODE" ]] || return 43 + shift 7 + printf '%s\\n' "$@" + fi + } + `; + for (const mode of ["-short", "-count=1"]) { + const covered = []; + for (const { shard, shards } of goTestPlan(["server/internal/query/query_test.go"]).matrix.include) { + const env = { + ...process.env, GO_TEST_AREA: "core", GO_TEST_MODE: mode, + TEST_SHARD: String(shard), TEST_SHARD_COUNT: String(shards), + MOCK_PACKAGES: packages.join("\n"), + }; + const result = spawnSync("bash", ["-c", `${mock}\n${step.run}`], { encoding: "utf8", env }); + assert.equal(result.status, 0, result.stderr); + const selected = result.stdout.trim().split("\n"); + assert.ok(selected.length >= 2 && selected.length <= 3); + covered.push(...selected); + const reordered = spawnSync("bash", ["-c", `${mock}\n${step.run}`], { + encoding: "utf8", env: { ...env, MOCK_PACKAGES: packages.toReversed().join("\n") }, + }); + assert.equal(reordered.status, 0, reordered.stderr); + assert.equal(reordered.stdout, result.stdout); + } + assert.deepEqual(covered.sort(), packages.sort()); + } + const empty = spawnSync("bash", ["-c", `go() { echo example/internal/server; }\n${step.run}`], { + encoding: "utf8", + env: { ...process.env, GO_TEST_AREA: "core", GO_TEST_MODE: "-short", TEST_SHARD: "3", TEST_SHARD_COUNT: "4" }, + }); + assert.notEqual(empty.status, 0); +}); + test("storage shards execute every discovered test exactly once with existing test modes", () => { const step = jobs["server-tests"].steps.find((step) => step.name === "Unit tests"); const names = [ diff --git a/tests/unit/workflow-contract-ci-triggers.test.mjs b/tests/unit/workflow-contract-ci-triggers.test.mjs index 50dd20db3c..33ce125712 100644 --- a/tests/unit/workflow-contract-ci-triggers.test.mjs +++ b/tests/unit/workflow-contract-ci-triggers.test.mjs @@ -127,11 +127,13 @@ test("Go dashboard integration shards complete test coverage across isolated ser assert.deepEqual(tests.env, { TEST_SHARD: "${{ matrix.shard }}", TEST_SHARD_COUNT: "${{ strategy.job-total }}", + GO_TEST_MODE: "${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '-count=1' || '-short' }}", }); assert.match(tests.run, /set -euo pipefail/); assert.match(tests.run, /test -n "\$pattern"/); assert.match(tests.run, /go -C server test -p 1 -run "\^\(\$pattern\)\$" \.\/\.\.\./); - assert.doesNotMatch(tests.run, /-timeout|-short|-skip/); + assert.match(tests.run, /go -C server test .* "\$GO_TEST_MODE"/); + assert.doesNotMatch(tests.run, /-timeout|-skip/); assert.equal(tests["continue-on-error"], undefined); assert.equal(tests.if, undefined); for (const name of [ @@ -152,14 +154,14 @@ test("Go dashboard integration shards complete test coverage across isolated ser const mock = [ "go() {", ' if [[ "$*" == *" -list "* ]]; then printf "%s\\n" "$MOCK_LIST";', - ' else printf "%s\\n" "$7"; fi', + ' else [[ "${!#}" == "$GO_TEST_MODE" ]] || return 42; printf "%s\\n" "$7"; fi', "}", ].join("\n"); for (const shard of job.strategy.matrix.shard) { const result = spawnSync("bash", ["-c", `${mock}\n${tests.run}`], { encoding: "utf8", env: { - ...process.env, MOCK_LIST: output, TEST_SHARD: String(shard), + ...process.env, MOCK_LIST: output, TEST_SHARD: String(shard), GO_TEST_MODE: "-count=1", TEST_SHARD_COUNT: String(job.strategy.matrix.shard.length), }, }); @@ -174,7 +176,7 @@ test("Go dashboard integration shards complete test coverage across isolated ser const empty = spawnSync("bash", ["-c", `${mock}\n${tests.run}`], { encoding: "utf8", env: { - ...process.env, MOCK_LIST: "ok example/empty", TEST_SHARD: "0", + ...process.env, MOCK_LIST: "ok example/empty", TEST_SHARD: "0", GO_TEST_MODE: "-short", TEST_SHARD_COUNT: String(job.strategy.matrix.shard.length), }, }); @@ -182,7 +184,7 @@ test("Go dashboard integration shards complete test coverage across isolated ser const failed = spawnSync("bash", ["-c", `go() { return 23; }\n${tests.run}`], { encoding: "utf8", env: { - ...process.env, TEST_SHARD: "0", + ...process.env, TEST_SHARD: "0", GO_TEST_MODE: "-short", TEST_SHARD_COUNT: String(job.strategy.matrix.shard.length), }, });