Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/scripts/affected-ci.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { pathToFileURL } from "node:url";

const tests = [
"discojs",
"discojs_node",
"discojs_web",
"server",
"webapp",
"cli",
"docs_examples",
];

function selectAll() {
return {
build: true,
docker: true,
any_tests: true,
...Object.fromEntries(tests.map((test) => [test, true])),
};
}

export function classifyPaths(paths) {
const selected = new Set();
let build = false;
let docker = false;

for (const path of paths) {
if (path.endsWith(".md") && !path.startsWith("docs/examples/")) continue;
if (path.startsWith("docs/") && !path.startsWith("docs/examples/"))
continue;

build = true;
if (path.startsWith("webapp/")) {
selected.add("webapp");
} else if (path.startsWith("cli/")) {
selected.add("cli");
} else if (path.startsWith("server/")) {
docker = true;
selected.add("server");
selected.add("cli");
selected.add("webapp");
selected.add("docs_examples");
} else if (path.startsWith("discojs-node/")) {
docker = true;
selected.add("discojs_node");
selected.add("server");
selected.add("cli");
selected.add("webapp");
selected.add("docs_examples");
} else if (path.startsWith("discojs-web/")) {
selected.add("discojs_web");
selected.add("webapp");
} else if (path.startsWith("docs/examples/")) {
selected.add("docs_examples");
} else if (path.startsWith("onnx-converter/")) {
// It has no dedicated test job, but the workspace build still covers it.
} else {
// Shared code, CI configuration, root files, and new paths get full CI.
docker = true;
tests.forEach((test) => selected.add(test));
}
}

return {
build,
docker,
any_tests: selected.size > 0,
...Object.fromEntries(tests.map((test) => [test, selected.has(test)])),
};
}

export function selectForEvent(eventName, ref, paths = []) {
if (
eventName === "merge_group" ||
(eventName === "push" && ref === "refs/heads/main")
) {
return selectAll();
}
if (eventName === "pull_request") return classifyPaths(paths);
throw new Error(`Unexpected event: ${eventName} on ${ref}`);
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const { GITHUB_EVENT_NAME, GITHUB_OUTPUT, BASE_SHA, HEAD_SHA } = process.env;
if (!GITHUB_OUTPUT) throw new Error("GITHUB_OUTPUT is required");

let paths = [];
if (GITHUB_EVENT_NAME === "pull_request") {
if (
!/^[a-f0-9]{40}$/.test(BASE_SHA ?? "") ||
!/^[a-f0-9]{40}$/.test(HEAD_SHA ?? "")
) {
throw new Error("Expected the PR base and head commit SHAs");
}
paths = execFileSync("git", [
"diff",
"--no-renames",
"--name-only",
"-z",
BASE_SHA,
HEAD_SHA,
])
.toString()
.split("\0")
.filter(Boolean);
}

const selected = selectForEvent(
GITHUB_EVENT_NAME,
process.env.GITHUB_REF,
paths,
);
appendFileSync(
GITHUB_OUTPUT,
Object.entries(selected)
.map(([name, enabled]) => `${name}=${enabled}\n`)
.join(""),
);
console.log("Changed paths:", paths);
console.log("CI jobs:", selected);
}
77 changes: 77 additions & 0 deletions .github/scripts/affected-ci.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { classifyPaths, selectForEvent } from "./affected-ci.mjs";

test("webapp changes skip CLI, server, and Docker", () => {
const result = classifyPaths(["webapp/src/App.vue"]);
assert.equal(result.webapp, true);
assert.equal(result.cli, false);
assert.equal(result.server, false);
assert.equal(result.docker, false);
});

test("server changes include its consumers", () => {
const result = classifyPaths(["server/src/main.ts"]);
assert.equal(result.server, true);
assert.equal(result.cli, true);
assert.equal(result.webapp, true);
assert.equal(result.docs_examples, true);
assert.equal(result.docker, true);
});

test("Node support changes reach CLI and server without running core tests", () => {
const result = classifyPaths(["discojs-node/src/index.ts"]);
assert.equal(result.discojs_node, true);
assert.equal(result.server, true);
assert.equal(result.cli, true);
assert.equal(result.webapp, true);
assert.equal(result.discojs, false);
assert.equal(result.docker, true);
});

test("root configuration and unfamiliar paths run everything", () => {
for (const path of [
"pnpm-lock.yaml",
"discojs/src/index.ts",
"new-package/src/index.ts",
]) {
const result = classifyPaths([path]);
assert.equal(result.docker, true);
assert.equal(result.discojs, true);
assert.equal(result.docs_examples, true);
}
});

test("mixed PR changes select the union of affected jobs", () => {
const result = selectForEvent("pull_request", "refs/pull/1/merge", [
"webapp/src/App.vue",
"cli/src/main.ts",
]);
assert.equal(result.webapp, true);
assert.equal(result.cli, true);
assert.equal(result.server, false);
assert.equal(result.docker, false);
});

test("merge groups and main pushes select the complete pipeline", () => {
for (const [event, ref] of [
["merge_group", "refs/heads/gh-readonly-queue/main/pr-1"],
["push", "refs/heads/main"],
]) {
const result = selectForEvent(event, ref);
assert.equal(result.build, true);
assert.equal(result.docker, true);
assert.equal(result.any_tests, true);
assert.equal(result.discojs, true);
assert.equal(result.server, true);
assert.equal(result.webapp, true);
assert.equal(result.cli, true);
assert.equal(result.docs_examples, true);
}
});

test("documentation-only changes leave verification to formatting", () => {
const result = classifyPaths(["docs/CONTRIBUTING.md"]);
assert.equal(result.build, false);
assert.equal(result.any_tests, false);
});
5 changes: 5 additions & 0 deletions .github/workflows/_build.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: build
on:
workflow_call:
inputs:
docker:
type: boolean
default: true

jobs:
build-server-docker:
if: inputs.docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
name: test
on:
workflow_call:
inputs:
discojs:
type: boolean
default: true
discojs_node:
type: boolean
default: true
discojs_web:
type: boolean
default: true
server:
type: boolean
default: true
webapp:
type: boolean
default: true
cli:
type: boolean
default: true
docs_examples:
type: boolean
default: true

jobs:
test-discojs:
if: inputs.discojs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -14,6 +37,7 @@ jobs:
- run: pnpm exec vitest --run --project=discojs

test-discojs-node:
if: inputs.discojs_node
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -24,6 +48,7 @@ jobs:
- run: pnpm exec vitest --run --project=discojs-node

test-discojs-web:
if: inputs.discojs_web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -34,6 +59,7 @@ jobs:
- run: pnpm exec vitest --run --project=discojs-web

test-server:
if: inputs.server
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -44,6 +70,7 @@ jobs:
- run: pnpm exec vitest --run --project=server

test-webapp:
if: inputs.webapp
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -54,6 +81,7 @@ jobs:
- run: pnpm -F webapp run test:unit

test-e2e:
if: inputs.webapp
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -68,6 +96,7 @@ jobs:
start: pnpm exec vite --mode test --port 1351

test-local-training:
if: inputs.webapp
name: local training (${{ matrix.task }})
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -97,6 +126,7 @@ jobs:
if-no-files-found: ignore

test-e2e-federated:
if: inputs.webapp
name: federated webapp (${{ matrix.task }})
runs-on: ubuntu-latest
timeout-minutes: 12
Expand Down Expand Up @@ -131,6 +161,7 @@ jobs:
if-no-files-found: ignore

test-cli:
if: inputs.cli
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -158,6 +189,7 @@ jobs:
pnpm -F cli start -t cifar10 -u 3 -e 1 -r 1 -i ci-cifar10

test-docs-examples:
if: inputs.docs_examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/check-formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ on:
push:
branches: [main]
pull_request:
merge_group:
types: [checks_requested]
branches: [main]

jobs:
format-check:
Expand Down
Loading
Loading