fix(ci): keep unused exports advisory (#105704)

* fix(ci): preserve frozen deadcode contracts

* fix(ci): keep unused exports advisory
This commit is contained in:
Vincent Koc
2026-07-13 05:47:05 +08:00
committed by GitHub
parent 7c72fef247
commit 2c06dfdd2f
3 changed files with 107 additions and 4 deletions
+12 -3
View File
@@ -1597,12 +1597,21 @@ jobs:
fi
;;
dependencies)
if pnpm run --silent 2>/dev/null | grep -q '^ deadcode:dependencies$'; then
has_package_script() {
node -e '
const scripts = require("./package.json").scripts ?? {};
process.exit(Object.hasOwn(scripts, process.argv[1]) ? 0 : 1);
' "$1"
}
if has_package_script "deadcode:dependencies" &&
has_package_script "deadcode:unused-files"; then
pnpm deadcode:dependencies
pnpm deadcode:unused-files
pnpm deadcode:exports
else
elif [[ "$HISTORICAL_TARGET" == "true" ]] && has_package_script "deadcode:ci"; then
pnpm deadcode:ci
else
echo "Target does not provide a supported deadcode check." >&2
exit 1
fi
;;
test-types)
+1 -1
View File
@@ -111,7 +111,7 @@ job budget.
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
The `check-dependencies` shard runs a production Knip dependency-only pass, the unused-file allowlist check, and the enforced unused-exports baseline ratchet. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-exports ratchet likewise rejects new findings and stale required baseline entries, so the checked-in debt can only shrink. The former ts-prune and ts-unused-exports advisory reports no longer run.
The `check-dependencies` shard runs a production Knip dependency-only pass and the unused-file allowlist check. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-exports baseline remains available as an advisory maintenance scan through `pnpm deadcode:exports`; after deleting dead code, regenerate it with `pnpm deadcode:exports:update`.
## ClawSweeper activity forwarding
+94
View File
@@ -385,6 +385,55 @@ function writeExecutable(filePath: string, lines: string[]): void {
chmodSync(filePath, 0o755);
}
function runDependencyCheckFixture(options: { historicalTarget: boolean; scripts: string[] }): {
calls: string[];
output: string;
status: number | null;
} {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-deadcode-"));
try {
const fakeBin = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
mkdirSync(fakeBin);
writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify({
scripts: Object.fromEntries(options.scripts.map((name) => [name, "true"])),
})}\n`,
);
writeExecutable(path.join(fakeBin, "pnpm"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "%s\\n" "$*" >> "$PNPM_CALLS"',
]);
const checkShardRun = readCiWorkflow().jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
).run;
const run = spawnSync("bash", ["-c", checkShardRun], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
FORMAT_CHECK: "false",
HISTORICAL_TARGET: options.historicalTarget ? "true" : "false",
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
PR_BASE_SHA: "",
TASK: "dependencies",
},
});
return {
calls: existsSync(callsPath)
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
: [],
output: `${run.stdout}${run.stderr}`,
status: run.status,
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function runGeneratedPublisherScenario(
baseChangePath: "a" | "b" | null,
options: {
@@ -1697,6 +1746,44 @@ describe("ci workflow guards", () => {
expect(preflightGuards).toContain("pnpm deps:patches:check");
});
it("uses stable deadcode checks for current and frozen checkouts", () => {
const modern = runDependencyCheckFixture({
historicalTarget: false,
scripts: ["deadcode:dependencies", "deadcode:unused-files", "deadcode:exports"],
});
expect(modern.status, modern.output).toBe(0);
expect(modern.calls).toEqual(["deadcode:dependencies", "deadcode:unused-files"]);
const frozen = runDependencyCheckFixture({
historicalTarget: true,
scripts: [
"deadcode:ci",
"deadcode:dependencies",
"deadcode:report:ci:ts-unused",
"deadcode:unused-files",
],
});
expect(frozen.status, frozen.output).toBe(0);
expect(frozen.calls).toEqual(["deadcode:dependencies", "deadcode:unused-files"]);
const legacy = runDependencyCheckFixture({
historicalTarget: true,
scripts: ["deadcode:ci"],
});
expect(legacy.status, legacy.output).toBe(0);
expect(legacy.calls).toEqual(["deadcode:ci"]);
const incompleteCurrent = runDependencyCheckFixture({
historicalTarget: false,
scripts: ["deadcode:dependencies"],
});
expect(incompleteCurrent.status).toBe(1);
expect(incompleteCurrent.calls).toEqual([]);
expect(incompleteCurrent.output).toContain(
"Target does not provide a supported deadcode check.",
);
});
it("runs mobile protocol coverage for Node and native-only changes", () => {
const workflow = readCiWorkflow();
const coverageStep = workflow.jobs.preflight.steps.find(
@@ -1978,6 +2065,13 @@ describe("ci workflow guards", () => {
);
expect(checkShard.run).toContain("pnpm tsgo:scripts");
expect(checkShard.run).toContain('elif [[ "$HISTORICAL_TARGET" != "true" ]]');
expect(checkShard.run).toContain('has_package_script "deadcode:dependencies"');
expect(checkShard.run).toContain('has_package_script "deadcode:unused-files"');
expect(checkShard.run).not.toContain('has_package_script "deadcode:exports"');
expect(checkShard.run).toContain(
'elif [[ "$HISTORICAL_TARGET" == "true" ]] && has_package_script "deadcode:ci"',
);
expect(checkShard.run).toContain("Target does not provide a supported deadcode check.");
const uiInstall = workflow.jobs["checks-ui"].steps.find(
(step: { name?: string }) => step.name === "Install Playwright Chromium",