@diffci.com/diffci 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
**Find test-selection opportunities in your CI before changing what it runs.** DiffCI analyzes a
|
|
8
8
|
commit's changes and dependency graph, then reports which test files it would select, why it falls
|
|
9
|
-
back to a full run, and whether it can propose a test command. The
|
|
9
|
+
back to a full run, and whether it can propose a test command. The `observe` command and Action are observation-only;
|
|
10
|
+
the opt-in `pilot` and `verify-savings` commands execute tests.
|
|
10
11
|
|
|
11
12
|
From an existing repository checkout, with Node.js 22.5+ and Git installed:
|
|
12
13
|
|
|
@@ -14,6 +15,23 @@ From an existing repository checkout, with Node.js 22.5+ and Git installed:
|
|
|
14
15
|
npx @diffci.com/diffci@latest observe --no-send
|
|
15
16
|
```
|
|
16
17
|
|
|
18
|
+
For the fastest self-serve runtime pilot, run one paired check from the repository root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @diffci.com/diffci@latest pilot --full "npm test"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
On Windows PowerShell, quote the package name:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
npx '@diffci.com/diffci@latest' pilot --full "npm test"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This executes the full and selected commands sequentially, and writes `diffci-observe.json`,
|
|
31
|
+
`diffci-savings.json`, and `diffci-savings.md` to a sibling `diffci-output` folder outside the checkout.
|
|
32
|
+
The commands you supply may create files or otherwise change the checkout. One paired run is preliminary
|
|
33
|
+
timing evidence; repeat comparisons and account for cache effects before claiming savings.
|
|
34
|
+
|
|
17
35
|
**Upgrade from 0.1.3:** tests excluded by a source-only `tsconfig.json` could be discovered without
|
|
18
36
|
their dependency edges, producing an incomplete selection. This is fixed in **0.1.4**. Revalidate
|
|
19
37
|
affected observations before using them as opportunity evidence; see the
|
|
@@ -34,8 +52,8 @@ not Cal.com's production savings or a prediction for your repository.
|
|
|
34
52
|
Selection counts alone do not establish runtime savings. Observation mode measures neither the
|
|
35
53
|
selected test execution nor realized savings.
|
|
36
54
|
|
|
37
|
-
For
|
|
38
|
-
observation report. It compares your normal full command with
|
|
55
|
+
For an advanced paired runtime check, you can still run `observe` first and then run `verify-savings`
|
|
56
|
+
against the observation report. It compares your normal full command with
|
|
39
57
|
DiffCI's proposed selected command and writes JSON plus Markdown evidence; see
|
|
40
58
|
[`docs/npm-adoption.md`](docs/npm-adoption.md#self-serve-runtime-pilot).
|
|
41
59
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { execFileSync } from "node:child_process";
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
|
-
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
32
32
|
import { observe, isInsideRepository } from "./observe.js";
|
|
33
33
|
import { submitObservation } from "./submit.js";
|
|
34
34
|
import { formatVerifySavingsSummary, runVerifySavings, writeVerifySavingsReport } from "./verify-savings.js";
|
|
@@ -286,9 +286,65 @@ function runVerifySavingsCommand(flags, env) {
|
|
|
286
286
|
console.log(` markdown: ${options.markdown}`);
|
|
287
287
|
return report.comparison.fullCommandSucceeded && report.comparison.selectedCommandSucceeded ? 0 : 1;
|
|
288
288
|
}
|
|
289
|
+
function defaultPilotOutputDir(repoPath) {
|
|
290
|
+
return join(dirname(repoPath), "diffci-output");
|
|
291
|
+
}
|
|
292
|
+
async function runPilot(flags, env) {
|
|
293
|
+
const full = stringFlag(flags, "full");
|
|
294
|
+
if (!full) {
|
|
295
|
+
console.error('--full <command> is required, for example: diffci pilot --full "npm test"');
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
const repoPath = resolve(stringFlag(flags, "repo") ?? env.GITHUB_WORKSPACE ?? process.cwd());
|
|
299
|
+
const outDir = resolve(stringFlag(flags, "out-dir") ?? defaultPilotOutputDir(repoPath));
|
|
300
|
+
if (isInsideRepository(repoPath, outDir) || outDir === repoPath) {
|
|
301
|
+
console.error(`Refusing to write pilot reports inside the repository: ${outDir}. Pass --out-dir with a path outside the checkout.`);
|
|
302
|
+
return 2;
|
|
303
|
+
}
|
|
304
|
+
const label = stringFlag(flags, "label") ?? basename(repoPath);
|
|
305
|
+
const observationPath = join(outDir, "diffci-observe.json");
|
|
306
|
+
const savingsPath = join(outDir, "diffci-savings.json");
|
|
307
|
+
const markdownPath = join(outDir, "diffci-savings.md");
|
|
308
|
+
const identity = observerIdentity();
|
|
309
|
+
const observation = await observe({
|
|
310
|
+
repoPath,
|
|
311
|
+
env: env,
|
|
312
|
+
version: identity.version,
|
|
313
|
+
engineSha: identity.sha,
|
|
314
|
+
baseOverride: stringFlag(flags, "base"),
|
|
315
|
+
headOverride: stringFlag(flags, "head"),
|
|
316
|
+
redactPaths: flags["redact-paths"] === true,
|
|
317
|
+
reportPath: observationPath,
|
|
318
|
+
});
|
|
319
|
+
mkdirSync(outDir, { recursive: true });
|
|
320
|
+
writeFileSync(observationPath, `${JSON.stringify(observation, null, 2)}\n`, "utf8");
|
|
321
|
+
console.log(summarise(observation));
|
|
322
|
+
console.log(` observation report: ${observationPath}`);
|
|
323
|
+
if (observation.status !== "OBSERVED") {
|
|
324
|
+
console.log("DiffCI pilot stopped before timing because observation did not produce a selectable report.");
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
const savings = runVerifySavings({
|
|
328
|
+
full,
|
|
329
|
+
selectedFromReport: observationPath,
|
|
330
|
+
out: savingsPath,
|
|
331
|
+
markdown: markdownPath,
|
|
332
|
+
label,
|
|
333
|
+
cwd: repoPath,
|
|
334
|
+
timeoutMs: numberFlag(flags, "timeout-ms") ?? 30 * 60 * 1000,
|
|
335
|
+
analysisOverheadMs: numberFlag(flags, "analysis-overhead-ms"),
|
|
336
|
+
tailBytes: numberFlag(flags, "tail-bytes") ?? 12_000,
|
|
337
|
+
});
|
|
338
|
+
writeVerifySavingsReport(savings, { out: savingsPath, markdown: markdownPath });
|
|
339
|
+
console.log(formatVerifySavingsSummary(savings));
|
|
340
|
+
console.log(` savings report: ${savingsPath}`);
|
|
341
|
+
console.log(` markdown: ${markdownPath}`);
|
|
342
|
+
return savings.comparison.fullCommandSucceeded && savings.comparison.selectedCommandSucceeded ? 0 : 1;
|
|
343
|
+
}
|
|
289
344
|
const USAGE = `diffci - observation-only change-aware CI analysis
|
|
290
345
|
|
|
291
346
|
Usage:
|
|
347
|
+
diffci pilot --full <command> [--repo <path>] [--out-dir <dir>] [--label <name>]
|
|
292
348
|
diffci observe [--repo <path>] [--out <file>] [--base <sha> --head <sha>]
|
|
293
349
|
[--redact-paths] [--json] [--quiet] [--fail-on-error]
|
|
294
350
|
[--api-url <url> --api-token <token>] [--no-send]
|
|
@@ -298,6 +354,7 @@ Usage:
|
|
|
298
354
|
diffci verify-workflow [--repo <path>]
|
|
299
355
|
diffci version
|
|
300
356
|
|
|
357
|
+
pilot runs observe and verify-savings together, writing reports to ../diffci-output by default.
|
|
301
358
|
observe analyses the checkout and writes one JSON report. It runs nothing and changes nothing.
|
|
302
359
|
verify-savings runs both commands and reports measured paired runtime; it is an opt-in pilot command.
|
|
303
360
|
verify-workflow checks that the job running DiffCI cannot affect any other job, and exits 1 if it can.
|
|
@@ -315,6 +372,9 @@ async function main() {
|
|
|
315
372
|
return;
|
|
316
373
|
}
|
|
317
374
|
switch (command) {
|
|
375
|
+
case "pilot":
|
|
376
|
+
process.exitCode = await runPilot(flags, env);
|
|
377
|
+
return;
|
|
318
378
|
case "observe":
|
|
319
379
|
process.exitCode = await runObserve(flags, env);
|
|
320
380
|
return;
|
|
@@ -333,8 +393,15 @@ async function main() {
|
|
|
333
393
|
}
|
|
334
394
|
}
|
|
335
395
|
main().catch((error) => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
396
|
+
const command = process.argv[2];
|
|
397
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
398
|
+
if (command === "observe") {
|
|
399
|
+
// Reaching here means a defect outside observe()'s own guard. It still must not take a build down:
|
|
400
|
+
// the failure is printed, and the exit code stays 0 unless the caller asked otherwise.
|
|
401
|
+
console.error(`DiffCI observer failed: ${message}`);
|
|
402
|
+
process.exitCode = process.argv.includes("--fail-on-error") ? 1 : 0;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
console.error(`DiffCI ${command ?? "command"} failed: ${message}`);
|
|
406
|
+
process.exitCode = 1;
|
|
340
407
|
});
|
|
@@ -11,11 +11,11 @@ function readSelectionFromObservation(path) {
|
|
|
11
11
|
const parsed = JSON.parse(readFileSync(absolutePath, "utf8"));
|
|
12
12
|
if (parsed.status !== "OBSERVED")
|
|
13
13
|
throw new Error(`--selected-from-report requires an OBSERVED report; got ${String(parsed.status)}`);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const commands = parsed.result?.proposedCommands;
|
|
15
|
+
if (!Array.isArray(commands) || commands.length !== 1 || typeof commands[0] !== "string" || !commands[0].trim()) {
|
|
16
|
+
throw new Error("--selected-from-report requires exactly one non-empty proposed command; use --selected with an explicit command covering the complete selection for multi-command plans");
|
|
17
|
+
}
|
|
18
|
+
const command = commands[0];
|
|
19
19
|
const selectedTests = Array.isArray(parsed.result?.selectedTests) ? parsed.result.selectedTests : undefined;
|
|
20
20
|
return {
|
|
21
21
|
command,
|
|
@@ -122,7 +122,9 @@ export function renderVerifySavingsMarkdown(report) {
|
|
|
122
122
|
const title = report.label ? `# DiffCI Verify Savings: ${report.label}` : "# DiffCI Verify Savings";
|
|
123
123
|
const warning = report.comparison.missedFailureSignal
|
|
124
124
|
? "\n> WARNING: Full failed while selected passed. Do not treat this selected command as safe until the full-run failure is understood.\n"
|
|
125
|
-
:
|
|
125
|
+
: !report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded
|
|
126
|
+
? "\n> WARNING: One or both commands failed. This comparison is invalid as savings evidence; timings below are diagnostic only.\n"
|
|
127
|
+
: "";
|
|
126
128
|
const selectionCounts = report.selectedTestCount !== undefined && report.totalTestCount !== undefined
|
|
127
129
|
? `\nSelected tests: ${report.selectedTestCount} of ${report.totalTestCount}\n`
|
|
128
130
|
: "";
|
|
@@ -137,6 +139,7 @@ This report compares a full command with a selected command on the same checkout
|
|
|
137
139
|
|
|
138
140
|
## Result
|
|
139
141
|
|
|
142
|
+
${!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded ? "Comparison invalid: command failure. Do not interpret the timing difference as savings.\n" : ""}
|
|
140
143
|
| Measure | Value |
|
|
141
144
|
| --- | ---: |
|
|
142
145
|
| Full runtime | ${formatMs(report.comparison.fullWallMs)} |
|
|
@@ -183,6 +186,10 @@ export function writeVerifySavingsReport(report, paths) {
|
|
|
183
186
|
writeText(paths.markdown, renderVerifySavingsMarkdown(report));
|
|
184
187
|
}
|
|
185
188
|
export function formatVerifySavingsSummary(report) {
|
|
189
|
+
if (!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandSucceeded) {
|
|
190
|
+
return "DiffCI verify-savings: comparison invalid because one or both commands failed" +
|
|
191
|
+
(report.comparison.missedFailureSignal ? "\n warning: full failed while selected passed; inspect outputs before claiming safety" : "");
|
|
192
|
+
}
|
|
186
193
|
const lines = [
|
|
187
194
|
`DiffCI verify-savings: ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} by ${formatMs(Math.abs(report.comparison.deltaMs))}`,
|
|
188
195
|
];
|
package/docs/npm-adoption.md
CHANGED
|
@@ -62,7 +62,7 @@ This is a paired local measurement, not a production-savings claim.
|
|
|
62
62
|
Step 1: create an observation report without sending it anywhere.
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npx @diffci.com/diffci@latest observe --no-send --out
|
|
65
|
+
npx @diffci.com/diffci@latest observe --no-send --out ../diffci-output/diffci-observation.json
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Step 2: run the paired pilot.
|
|
@@ -72,13 +72,17 @@ npx @diffci.com/diffci@latest verify-savings \
|
|
|
72
72
|
--label owner/repo \
|
|
73
73
|
--repo /path/to/their/repo \
|
|
74
74
|
--full "npm test" \
|
|
75
|
-
--selected-from-report /
|
|
76
|
-
--out
|
|
77
|
-
--markdown
|
|
75
|
+
--selected-from-report ../diffci-output/diffci-observation.json \
|
|
76
|
+
--out ../diffci-output/diffci-verify-savings.json \
|
|
77
|
+
--markdown ../diffci-output/diffci-verify-savings.md
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
What the report means:
|
|
81
81
|
|
|
82
|
+
Run both steps from the same repository root with the same checked-out revision. These commands
|
|
83
|
+
execute repository code. The full run can warm caches for the selected run, so repeat comparisons
|
|
84
|
+
with controlled cache state before drawing conclusions. A passing pair does not establish selection safety.
|
|
85
|
+
|
|
82
86
|
- Full runtime is measured from `--full`.
|
|
83
87
|
- Selected runtime is measured from DiffCI's proposed command in the observation report.
|
|
84
88
|
- DiffCI analysis overhead is imported from `timings.totalMs` in the observation report unless
|