Skip to content

Commit 2c06dfd

Browse files
authored
fix(ci): keep unused exports advisory (#105704)
* fix(ci): preserve frozen deadcode contracts * fix(ci): keep unused exports advisory
1 parent 7c72fef commit 2c06dfd

3 files changed

Lines changed: 107 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,12 +1597,21 @@ jobs:
15971597
fi
15981598
;;
15991599
dependencies)
1600-
if pnpm run --silent 2>/dev/null | grep -q '^ deadcode:dependencies$'; then
1600+
has_package_script() {
1601+
node -e '
1602+
const scripts = require("./package.json").scripts ?? {};
1603+
process.exit(Object.hasOwn(scripts, process.argv[1]) ? 0 : 1);
1604+
' "$1"
1605+
}
1606+
if has_package_script "deadcode:dependencies" &&
1607+
has_package_script "deadcode:unused-files"; then
16011608
pnpm deadcode:dependencies
16021609
pnpm deadcode:unused-files
1603-
pnpm deadcode:exports
1604-
else
1610+
elif [[ "$HISTORICAL_TARGET" == "true" ]] && has_package_script "deadcode:ci"; then
16051611
pnpm deadcode:ci
1612+
else
1613+
echo "Target does not provide a supported deadcode check." >&2
1614+
exit 1
16061615
fi
16071616
;;
16081617
test-types)

docs/ci.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ job budget.
111111

112112
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.
113113

114-
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.
114+
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`.
115115

116116
## ClawSweeper activity forwarding
117117

test/scripts/ci-workflow-guards.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,55 @@ function writeExecutable(filePath: string, lines: string[]): void {
385385
chmodSync(filePath, 0o755);
386386
}
387387

388+
function runDependencyCheckFixture(options: { historicalTarget: boolean; scripts: string[] }): {
389+
calls: string[];
390+
output: string;
391+
status: number | null;
392+
} {
393+
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-deadcode-"));
394+
try {
395+
const fakeBin = path.join(root, "bin");
396+
const callsPath = path.join(root, "pnpm-calls.txt");
397+
mkdirSync(fakeBin);
398+
writeFileSync(
399+
path.join(root, "package.json"),
400+
`${JSON.stringify({
401+
scripts: Object.fromEntries(options.scripts.map((name) => [name, "true"])),
402+
})}\n`,
403+
);
404+
writeExecutable(path.join(fakeBin, "pnpm"), [
405+
"#!/usr/bin/env bash",
406+
"set -euo pipefail",
407+
'printf "%s\\n" "$*" >> "$PNPM_CALLS"',
408+
]);
409+
const checkShardRun = readCiWorkflow().jobs["check-shard"].steps.find(
410+
(step: WorkflowStep) => step.name === "Run check shard",
411+
).run;
412+
const run = spawnSync("bash", ["-c", checkShardRun], {
413+
cwd: root,
414+
encoding: "utf8",
415+
env: {
416+
...process.env,
417+
FORMAT_CHECK: "false",
418+
HISTORICAL_TARGET: options.historicalTarget ? "true" : "false",
419+
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
420+
PNPM_CALLS: callsPath,
421+
PR_BASE_SHA: "",
422+
TASK: "dependencies",
423+
},
424+
});
425+
return {
426+
calls: existsSync(callsPath)
427+
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
428+
: [],
429+
output: `${run.stdout}${run.stderr}`,
430+
status: run.status,
431+
};
432+
} finally {
433+
rmSync(root, { force: true, recursive: true });
434+
}
435+
}
436+
388437
function runGeneratedPublisherScenario(
389438
baseChangePath: "a" | "b" | null,
390439
options: {
@@ -1697,6 +1746,44 @@ describe("ci workflow guards", () => {
16971746
expect(preflightGuards).toContain("pnpm deps:patches:check");
16981747
});
16991748

1749+
it("uses stable deadcode checks for current and frozen checkouts", () => {
1750+
const modern = runDependencyCheckFixture({
1751+
historicalTarget: false,
1752+
scripts: ["deadcode:dependencies", "deadcode:unused-files", "deadcode:exports"],
1753+
});
1754+
expect(modern.status, modern.output).toBe(0);
1755+
expect(modern.calls).toEqual(["deadcode:dependencies", "deadcode:unused-files"]);
1756+
1757+
const frozen = runDependencyCheckFixture({
1758+
historicalTarget: true,
1759+
scripts: [
1760+
"deadcode:ci",
1761+
"deadcode:dependencies",
1762+
"deadcode:report:ci:ts-unused",
1763+
"deadcode:unused-files",
1764+
],
1765+
});
1766+
expect(frozen.status, frozen.output).toBe(0);
1767+
expect(frozen.calls).toEqual(["deadcode:dependencies", "deadcode:unused-files"]);
1768+
1769+
const legacy = runDependencyCheckFixture({
1770+
historicalTarget: true,
1771+
scripts: ["deadcode:ci"],
1772+
});
1773+
expect(legacy.status, legacy.output).toBe(0);
1774+
expect(legacy.calls).toEqual(["deadcode:ci"]);
1775+
1776+
const incompleteCurrent = runDependencyCheckFixture({
1777+
historicalTarget: false,
1778+
scripts: ["deadcode:dependencies"],
1779+
});
1780+
expect(incompleteCurrent.status).toBe(1);
1781+
expect(incompleteCurrent.calls).toEqual([]);
1782+
expect(incompleteCurrent.output).toContain(
1783+
"Target does not provide a supported deadcode check.",
1784+
);
1785+
});
1786+
17001787
it("runs mobile protocol coverage for Node and native-only changes", () => {
17011788
const workflow = readCiWorkflow();
17021789
const coverageStep = workflow.jobs.preflight.steps.find(
@@ -1978,6 +2065,13 @@ describe("ci workflow guards", () => {
19782065
);
19792066
expect(checkShard.run).toContain("pnpm tsgo:scripts");
19802067
expect(checkShard.run).toContain('elif [[ "$HISTORICAL_TARGET" != "true" ]]');
2068+
expect(checkShard.run).toContain('has_package_script "deadcode:dependencies"');
2069+
expect(checkShard.run).toContain('has_package_script "deadcode:unused-files"');
2070+
expect(checkShard.run).not.toContain('has_package_script "deadcode:exports"');
2071+
expect(checkShard.run).toContain(
2072+
'elif [[ "$HISTORICAL_TARGET" == "true" ]] && has_package_script "deadcode:ci"',
2073+
);
2074+
expect(checkShard.run).toContain("Target does not provide a supported deadcode check.");
19812075

19822076
const uiInstall = workflow.jobs["checks-ui"].steps.find(
19832077
(step: { name?: string }) => step.name === "Install Playwright Chromium",

0 commit comments

Comments
 (0)