fix(ci): require portable inventory reports on selected runs (#3549)

Run complete portable-inventory validation whenever code tests are selected and the job is not cancelled. Missing timing reports now fail rather than bypassing the existing validator. Preserve partial diagnostics and successful-history ownership.

Verified the baseline regression, missing/present report and cancellation/selection controls, actual Vitest blob/report failure paths, static checks, actionlint and test discovery.

Fixes #3542

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-15 00:16:09 +08:00
1 parent 044fb9127a
commit 0f623732bd
2 files changed
+39 -2

No files matched your search

+1 -1
View File
@@ -433,7 +433,7 @@ jobs:
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Summarize slowest test files
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-timings.json') != ''
if: needs.changes.outputs.code == 'true' && !cancelled()
env:
SHARD_RESULT: ${{ needs.test-shard.result }}
SOURCE_REQUIRED: ${{ needs.changes.outputs.tier == 'fast' }}
@@ -140,13 +140,18 @@ function conditionValue(
condition: string | undefined,
needs: Record<string, unknown>,
canceled: boolean,
timingReportExists = false,
) {
if (!condition) throw new Error("Missing report collection condition");
return runInNewContext(
condition
.replaceAll("needs.test-shard", 'needs["test-shard"]')
.replaceAll("needs.test-source", 'needs["test-source"]'),
{ needs, cancelled: () => canceled },
{
needs,
cancelled: () => canceled,
hashFiles: () => (timingReportExists ? "timing-report" : ""),
},
{ timeout: 1_000 },
);
}
@@ -207,6 +212,28 @@ it("collects either fast test group while keeping full reports tied to full shar
);
});
it("requires portable report validation even when the timing report is missing", async () => {
const summary = (await workflow()).test.steps.find((step) =>
step.run?.includes("scripts/ci/test-report.mjs"),
);
for (const tier of ["fast", "full"]) {
for (const timingReportExists of [false, true]) {
for (const code of ["true", "false"]) {
for (const canceled of [false, true]) {
expect(
conditionValue(
summary?.if,
{ changes: { outputs: { code, tier } } },
canceled,
timingReportExists,
),
).toBe(code === "true" && !canceled);
}
}
}
}
});
it("keeps diagnosis available after merge failure and publishes only complete successful history", async () => {
const steps = (await workflow()).test!.steps;
const diagnosis = steps.find((step) => step.with?.name === "test-results");
@@ -270,6 +297,16 @@ it("rejects successful shard reports with missing or duplicate portable files",
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
}
await run(["tests/first.test.ts", "tests/second.test.ts"]);
await rm(join(root, "test-timings.json"));
await expect(
executeTimingReport(root, { CI_VERIFY_TEST_INVENTORY: "true" }),
).rejects.toMatchObject({
stderr: expect.stringContaining("ENOENT"),
});
await expect(
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
it("runs the existing onboarding and Python producer guards from their selected CI owners", async () => {