ci: run cli tests in their own job so runtime tests stop timing out (#6326)

* ci: run cli tests in their own job so runtime tests stop timing out

* ci: format the test aggregator needs list

* ci: keep the test aggregator needs on one line for the reachability check

* ci(reachability): read wrapped and block needs lists instead of rejecting them
This commit is contained in:
Miguel Ángel
2026-10-25 18:54:01 +00:00
committed by GitHub
parent 1a37db4aee
commit 8214cdcd29
3 changed files with 60 additions and 14 deletions
+35 -4
View File
@@ -575,7 +575,29 @@ jobs:
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
- run: bun run --cwd packages/core build
- run: bun run --cwd packages/core build:hyperframes-runtime
- run: bun run --filter '@hyperframes/{cli,core,engine}' test
- run: bun run --filter '@hyperframes/{core,engine}' test
test-cli:
name: Test (cli)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
lfs: true
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- uses: ./.github/actions/install-ffmpeg-linux
- uses: ./.github/actions/prepare-ffmpeg-bin
- run: bash scripts/ci/install-workspace-dependencies.sh
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
- run: bun run --cwd packages/core build
- run: bun run --cwd packages/core build:hyperframes-runtime
- run: bun run --filter '@hyperframes/cli' test
test-support:
name: Test (support)
@@ -612,7 +634,16 @@ jobs:
test:
name: Test
needs: [changes, producer-source-tests, test-studio, test-runtime, test-support, test-skills]
needs:
[
changes,
producer-source-tests,
test-studio,
test-runtime,
test-cli,
test-support,
test-skills,
]
# Keep the existing required `Test` context authoritative for producer
# failures too. It also fails closed if any package shard fails or is cancelled.
if: always() && (needs.changes.result != 'success' || needs.changes.outputs.code == 'true' || needs.changes.outputs.skills == 'true')
@@ -630,10 +661,10 @@ jobs:
echo "::error::Producer unit/integration tests did not succeed."
exit 1
- name: Require package test shards
if: needs.changes.outputs.code == 'true' && (needs.test-studio.result != 'success' || needs.test-runtime.result != 'success' || needs.test-support.result != 'success')
if: needs.changes.outputs.code == 'true' && (needs.test-studio.result != 'success' || needs.test-runtime.result != 'success' || needs.test-cli.result != 'success' || needs.test-support.result != 'success')
run: |
echo "::error::A package test shard did not succeed."
echo "studio=${{ needs.test-studio.result }} runtime=${{ needs.test-runtime.result }} support=${{ needs.test-support.result }}"
echo "studio=${{ needs.test-studio.result }} runtime=${{ needs.test-runtime.result }} cli=${{ needs.test-cli.result }} support=${{ needs.test-support.result }}"
exit 1
- name: Require skills tests
if: needs.changes.outputs.skills == 'true' && needs.test-skills.result != 'success'
+13 -8
View File
@@ -58,24 +58,29 @@ function jobField(body, key) {
return body.match(new RegExp(`^ ${key}: (.*)$`, "m"))?.[1] ?? "";
}
// `needs` as a one-line list, a multi-line list or a block list, whichever the formatter wrote.
function jobNeeds(body) {
const value = body.match(/^ needs:(.*(?:\n(?: .*| *))*)/m)?.[1] ?? "";
return value
.replace(/#.*$/gm, "")
.replace(/^\s*- /gm, "")
.split(/[\s,[\]]+/)
.filter(Boolean);
}
function readJob(block) {
const name = block.match(/^ ([\w-]+):/)[1];
const body = block.slice(block.indexOf("\n") + 1);
const condition = jobField(body, "if");
const needs = jobField(body, "needs");
return {
name,
body,
condition,
needs: needs.replace(/[[\]]/g, "").split(/,\s*/).filter(Boolean),
};
const needs = jobNeeds(body);
return { name, body, condition, needs };
}
// Reject unsupported control syntax instead of treating it as an unconditional job.
export function readWorkflow(source) {
if (/^\s+(?:paths|paths-ignore|branches-ignore):/m.test(source.split("jobs:\n")[0]))
throw new Error("Workflow trigger restrictions need an explicit reachability model");
if (/^ (?:needs|if):\s*$/m.test(source))
if (/^ if:\s*$/m.test(source) || /^ needs:\s*\n (?![-[])/m.test(source))
throw new Error("Block job conditions and dependencies need an explicit reachability model");
if (/^\s+(?:- )?working-directory:/m.test(source))
throw new Error("Working-directory overrides need an explicit reachability model");
+12 -2
View File
@@ -137,11 +137,21 @@ test("conditions in the first step key are never credited", () => {
assert.match(check(tree)["scripts/parity.test.mjs"][0], /no CI runner/);
});
test("block dependency syntax fails closed instead of losing the dependency", () => {
test("block and wrapped dependency lists gate a job like the one-line list", () => {
for (const needs of ["\n - disabled", "\n [\n disabled,\n ]"]) {
const tree = fixture("node --test scripts/parity.test.mjs", '"**"');
tree[".github/workflows/ci.yml"] =
tree[".github/workflows/ci.yml"].replace(" needs: changes", ` needs:${needs}`) +
" disabled:\n if: false\n steps: []\n";
assert.match(check(tree)["scripts/parity.test.mjs"].join("\n"), /CI filters exclude/);
}
});
test("a dependency mapping fails closed instead of losing the dependency", () => {
const tree = fixture();
tree[".github/workflows/ci.yml"] = tree[".github/workflows/ci.yml"].replace(
"needs: changes",
"needs:\n - disabled",
"needs:\n disabled: x",
);
assert.throws(() => check(tree), /Block job/);
});