mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(ci): don't run desktop tests for purely mobile client changes (#7709)
Mobile-only PRs currently trigger desktop CI because the desktop test filter's glob inadvertently matches every path outside Tauri. Fix that filter so that pure mobile changes skip desktop builds, tests, and their relay artifact producer. ### Verification - 12 regression tests exercise the exact pinned paths-filter action on mobile, desktop, Tauri, backend, mixed, workflow, and documentation changes. - Restoring the bad glob makes the mobile-only regression test fail. - The filter tests, existing required-context isolation contract, workflow syntax validation, and Biome pass locally. --------- Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -63,7 +63,6 @@ jobs:
|
||||
- 'scripts/model-capabilities.json'
|
||||
- 'scripts/normative-corpus.json'
|
||||
- 'desktop/**'
|
||||
- '!desktop/src-tauri/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
desktop-rust:
|
||||
- 'desktop/src-tauri/**'
|
||||
@@ -114,6 +113,8 @@ jobs:
|
||||
scripts/test-rust-cache-contract-regressions.sh
|
||||
- name: CI required-context isolation contract
|
||||
run: scripts/test-ci-required-context-isolation.sh
|
||||
- name: CI path selection regression tests
|
||||
run: node --test scripts/ci-selection.test.mjs
|
||||
- name: File size policy
|
||||
run: just file-size-check
|
||||
|
||||
|
||||
140
scripts/ci-selection.test.mjs
Normal file
140
scripts/ci-selection.test.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { after, test } from "node:test";
|
||||
|
||||
const workflow = readFileSync(
|
||||
new URL("../.github/workflows/ci.yml", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const scratch = mkdtempSync(join(tmpdir(), "buzz-ci-selection-"));
|
||||
after(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
const actionSha = workflow.match(/uses: dorny\/paths-filter@([a-f0-9]{40})/)[1];
|
||||
const filters = workflow
|
||||
.match(/ {10}filters: \|\n([\s\S]*?)(?= {6}- name:)/)[1]
|
||||
.split("\n")
|
||||
.map((line) => line.slice(12))
|
||||
.join("\n");
|
||||
// GitHub-hosted runners prepare pinned actions beside RUNNER_TEMP before any
|
||||
// steps run. Reuse that bundle, without another download in the selection gate.
|
||||
// Locally, point PATHS_FILTER_ACTION at dist/index.js from the pinned action.
|
||||
const actionPath =
|
||||
process.env.PATHS_FILTER_ACTION ||
|
||||
(process.env.RUNNER_TEMP &&
|
||||
join(
|
||||
process.env.RUNNER_TEMP,
|
||||
"..",
|
||||
"_actions",
|
||||
"dorny",
|
||||
"paths-filter",
|
||||
actionSha,
|
||||
"dist",
|
||||
"index.js",
|
||||
));
|
||||
assert.ok(
|
||||
actionPath && existsSync(actionPath),
|
||||
`Set PATHS_FILTER_ACTION to the local dist/index.js from dorny/paths-filter@${actionSha}`,
|
||||
);
|
||||
function select(paths) {
|
||||
const repo = mkdtempSync(join(scratch, "repo-"));
|
||||
const git = (...args) =>
|
||||
execFileSync("git", args, { cwd: repo, stdio: "pipe", timeout: 10000 });
|
||||
git("init", "-q");
|
||||
// Fixture repositories must not inherit developer machine hooks.
|
||||
mkdirSync(join(repo, "empty-hooks"));
|
||||
git("config", "core.hooksPath", join(repo, "empty-hooks"));
|
||||
git(
|
||||
"-c",
|
||||
"user.name=CI fixture",
|
||||
"-c",
|
||||
"user.email=ci@example.com",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
"commit",
|
||||
"-q",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"fixture",
|
||||
"-s",
|
||||
);
|
||||
for (const path of paths) {
|
||||
mkdirSync(dirname(join(repo, path)), { recursive: true });
|
||||
writeFileSync(join(repo, path), "fixture\n");
|
||||
}
|
||||
git("add", ".");
|
||||
const output = join(repo, "action-output");
|
||||
writeFileSync(output, "");
|
||||
const child = spawnSync(process.execPath, [actionPath], {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
timeout: 30000,
|
||||
env: {
|
||||
...process.env,
|
||||
INPUT_BASE: "HEAD",
|
||||
INPUT_FILTERS: filters,
|
||||
INPUT_TOKEN: "",
|
||||
INPUT_REF: "",
|
||||
GITHUB_OUTPUT: output,
|
||||
"INPUT_PREDICATE-QUANTIFIER":
|
||||
workflow.match(/predicate-quantifier: ['"]?([\w-]+)/)?.[1] ?? "some",
|
||||
},
|
||||
});
|
||||
assert.equal(child.status, 0, child.stdout + child.stderr);
|
||||
return Object.fromEntries(
|
||||
[
|
||||
...readFileSync(output, "utf8").matchAll(
|
||||
/^(rust|desktop|desktop-rust|web|mobile)<<([^\n]+)\n(true|false)\n\2/gm,
|
||||
),
|
||||
].map(([, key, , value]) => [key, value]),
|
||||
);
|
||||
}
|
||||
const scenarios = [
|
||||
[
|
||||
"mobile source and tests",
|
||||
[
|
||||
"mobile/lib/features/age_gate/age_signal_provider.dart",
|
||||
"mobile/test/features/age_gate/age_signal_provider_test.dart",
|
||||
],
|
||||
["mobile"],
|
||||
],
|
||||
["mobile lockfile", ["mobile/pubspec.lock"], ["mobile"]],
|
||||
["mobile release script", ["scripts/mobile-release.sh"], ["mobile"]],
|
||||
["desktop", ["desktop/src/main.tsx"], ["desktop"]],
|
||||
["Tauri", ["desktop/src-tauri/src/main.rs"], ["desktop", "desktop-rust"]],
|
||||
["relay", ["crates/buzz-relay/src/main.rs"], ["rust"]],
|
||||
["migration", ["migrations/123.sql"], ["rust"]],
|
||||
["shared workflow", [".github/workflows/ci.yml"], ["rust", "mobile"]],
|
||||
[
|
||||
"mixed mobile and relay",
|
||||
["mobile/lib/main.dart", "crates/buzz-core/src/lib.rs"],
|
||||
["rust", "mobile"],
|
||||
],
|
||||
[
|
||||
"mixed mobile and desktop",
|
||||
["mobile/lib/main.dart", "desktop/src/main.tsx"],
|
||||
["desktop", "mobile"],
|
||||
],
|
||||
["web", ["web/src/main.tsx"], ["web"]],
|
||||
["documentation", ["README.md"], []],
|
||||
];
|
||||
for (const [name, paths, expected] of scenarios) {
|
||||
test(`real paths-filter: ${name}`, () => {
|
||||
const outputs = select(paths);
|
||||
assert.equal(Object.keys(outputs).length, 5);
|
||||
assert.deepEqual(
|
||||
Object.keys(outputs)
|
||||
.filter((key) => outputs[key] === "true")
|
||||
.sort(),
|
||||
expected.sort(),
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user