ccqa 0.11.0 → 0.13.0
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/dist/bin/ccqa.mjs +321 -49
- package/dist/hub-client/index.d.mts +17 -0
- package/dist/hub-client/index.mjs +2 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -2630,6 +2630,7 @@ const ReportSpecResultSchema = z.object({
|
|
|
2630
2630
|
});
|
|
2631
2631
|
const RunReportDataSchema = z.object({
|
|
2632
2632
|
schemaVersion: z.literal(1),
|
|
2633
|
+
kind: z.enum(["run", "drift"]).default("run"),
|
|
2633
2634
|
createdAt: z.string(),
|
|
2634
2635
|
runId: z.string().nullable(),
|
|
2635
2636
|
git: z.object({
|
|
@@ -3198,6 +3199,44 @@ function resolveProjectOrThrow(project, cwd) {
|
|
|
3198
3199
|
*/
|
|
3199
3200
|
const hubUrlOption = ["--hub-url <url>", "ccqa hub base URL (or CCQA_HUB_URL)."];
|
|
3200
3201
|
const hubTokenOption = ["--hub-token <token>", "ccqa hub bearer token (or CCQA_HUB_TOKEN)."];
|
|
3202
|
+
const hubHeaderOption = [
|
|
3203
|
+
"--hub-header <header>",
|
|
3204
|
+
"Extra header sent with every hub request, as \"key:value\" (or CCQA_HUB_HEADER). Repeatable. For infra that gates the hub behind a header check (e.g. a load balancer bypass rule).",
|
|
3205
|
+
collectHubHeader,
|
|
3206
|
+
[]
|
|
3207
|
+
];
|
|
3208
|
+
/** commander `--hub-header` accumulator: collects repeated flags into an array. */
|
|
3209
|
+
function collectHubHeader(value, previous) {
|
|
3210
|
+
return [...previous, value];
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Parse `"key:value"` entries (from `--hub-header`, repeatable) into a
|
|
3214
|
+
* headers map. The value may itself contain `:` (e.g. a URL), so only the
|
|
3215
|
+
* first colon is treated as the separator. Throws on an entry with no colon.
|
|
3216
|
+
*/
|
|
3217
|
+
function parseHubHeaders(entries) {
|
|
3218
|
+
const headers = {};
|
|
3219
|
+
for (const entry of entries) {
|
|
3220
|
+
const i = entry.indexOf(":");
|
|
3221
|
+
if (i < 0) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
|
|
3222
|
+
const key = entry.slice(0, i).trim();
|
|
3223
|
+
const value = entry.slice(i + 1).trim();
|
|
3224
|
+
if (!key) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
|
|
3225
|
+
headers[key] = value;
|
|
3226
|
+
}
|
|
3227
|
+
return headers;
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Resolve custom hub headers from flags, falling back to `CCQA_HUB_HEADER`
|
|
3231
|
+
* (a single `"key:value"` entry) when no `--hub-header` flag was given.
|
|
3232
|
+
* CI setups typically pass exactly one gateway header via env; `--hub-header`
|
|
3233
|
+
* is repeatable for the (rarer) local/multi-header case.
|
|
3234
|
+
*/
|
|
3235
|
+
function resolveHubHeaders(hubHeader) {
|
|
3236
|
+
if (hubHeader && hubHeader.length > 0) return parseHubHeaders(hubHeader);
|
|
3237
|
+
const envHeader = process.env.CCQA_HUB_HEADER;
|
|
3238
|
+
if (envHeader) return parseHubHeaders([envHeader]);
|
|
3239
|
+
}
|
|
3201
3240
|
/** Thrown by `requireHubClient` when the URL and/or token can't be resolved from flags/env. */
|
|
3202
3241
|
var HubConnectionError = class extends Error {
|
|
3203
3242
|
constructor(message) {
|
|
@@ -3214,9 +3253,11 @@ function resolveHubClient(opts) {
|
|
|
3214
3253
|
const baseUrl = opts.hubUrl ?? process.env.CCQA_HUB_URL;
|
|
3215
3254
|
const token = opts.hubToken ?? process.env.CCQA_HUB_TOKEN;
|
|
3216
3255
|
if (!baseUrl || !token) return null;
|
|
3256
|
+
const headers = resolveHubHeaders(opts.hubHeader);
|
|
3217
3257
|
return createHubClient({
|
|
3218
3258
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
3219
|
-
token
|
|
3259
|
+
token,
|
|
3260
|
+
...headers ? { headers } : {}
|
|
3220
3261
|
});
|
|
3221
3262
|
}
|
|
3222
3263
|
/** Same as `resolveHubClient`, but throws `HubConnectionError` instead of returning `null`. */
|
|
@@ -3242,6 +3283,25 @@ function resolveHubContext(opts) {
|
|
|
3242
3283
|
project: resolveProjectOrThrow(opts.project, opts.cwd ?? process.cwd())
|
|
3243
3284
|
};
|
|
3244
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Wrap a subcommand action so a `HubApiError` (hub request failed, e.g. a
|
|
3288
|
+
* 503 when the hub has no encryption key configured) prints a clean message
|
|
3289
|
+
* and exits 2, instead of surfacing as an unhandled rejection with a stack
|
|
3290
|
+
* trace.
|
|
3291
|
+
*/
|
|
3292
|
+
function withHubErrors(fn) {
|
|
3293
|
+
return async (...args) => {
|
|
3294
|
+
try {
|
|
3295
|
+
await fn(...args);
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
if (err instanceof HubApiError) {
|
|
3298
|
+
error(`hub request failed (${err.status} ${err.code}): ${err.message}`);
|
|
3299
|
+
process.exit(2);
|
|
3300
|
+
}
|
|
3301
|
+
throw err;
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3245
3305
|
//#endregion
|
|
3246
3306
|
//#region src/cli/options.ts
|
|
3247
3307
|
/**
|
|
@@ -3266,7 +3326,7 @@ function addProfileOption(command) {
|
|
|
3266
3326
|
* options so help text and behaviour don't drift.
|
|
3267
3327
|
*/
|
|
3268
3328
|
function addHubOptions(command) {
|
|
3269
|
-
return command.option(...hubUrlOption).option(...hubTokenOption);
|
|
3329
|
+
return command.option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption);
|
|
3270
3330
|
}
|
|
3271
3331
|
/**
|
|
3272
3332
|
* CLI wrapper around `resolveProfileEnv`: on failure, print the error and
|
|
@@ -3856,6 +3916,33 @@ function resolvePromptLocalPath(name, cwd) {
|
|
|
3856
3916
|
return join(cwd ?? process.cwd(), PROMPT_LOCAL_PATHS[name]);
|
|
3857
3917
|
}
|
|
3858
3918
|
//#endregion
|
|
3919
|
+
//#region src/cli/git-branch.ts
|
|
3920
|
+
/** Best-effort current branch: CI env vars first, then git, else null. */
|
|
3921
|
+
async function detectBranch(cwd) {
|
|
3922
|
+
const fromEnv = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME;
|
|
3923
|
+
if (fromEnv) return fromEnv;
|
|
3924
|
+
try {
|
|
3925
|
+
const { stdout } = await execFileP("git", [
|
|
3926
|
+
"rev-parse",
|
|
3927
|
+
"--abbrev-ref",
|
|
3928
|
+
"HEAD"
|
|
3929
|
+
], { cwd });
|
|
3930
|
+
const branch = stdout.trim();
|
|
3931
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
3932
|
+
} catch {
|
|
3933
|
+
return null;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
/** Best-effort current commit SHA, or null (e.g. not a git repo). */
|
|
3937
|
+
async function getGitHead(cwd) {
|
|
3938
|
+
try {
|
|
3939
|
+
const { stdout } = await execFileP("git", ["rev-parse", "HEAD"], { cwd });
|
|
3940
|
+
return stdout.trim() || null;
|
|
3941
|
+
} catch {
|
|
3942
|
+
return null;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
//#endregion
|
|
3859
3946
|
//#region src/cli/hub.ts
|
|
3860
3947
|
/**
|
|
3861
3948
|
* `ccqa hub` — the client side of the ccqa hub (a results/secret control
|
|
@@ -3904,25 +3991,6 @@ async function readStdin() {
|
|
|
3904
3991
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
3905
3992
|
return Buffer.concat(chunks).toString("utf8");
|
|
3906
3993
|
}
|
|
3907
|
-
/**
|
|
3908
|
-
* Wrap a subcommand action so a `HubApiError` (hub request failed, e.g. a
|
|
3909
|
-
* 503 when the hub has no encryption key configured) prints a clean message
|
|
3910
|
-
* and exits 2, instead of surfacing as an unhandled rejection with a stack
|
|
3911
|
-
* trace.
|
|
3912
|
-
*/
|
|
3913
|
-
function withHubErrors(fn) {
|
|
3914
|
-
return async (...args) => {
|
|
3915
|
-
try {
|
|
3916
|
-
await fn(...args);
|
|
3917
|
-
} catch (err) {
|
|
3918
|
-
if (err instanceof HubApiError) {
|
|
3919
|
-
error(`hub request failed (${err.status} ${err.code}): ${err.message}`);
|
|
3920
|
-
process.exit(2);
|
|
3921
|
-
}
|
|
3922
|
-
throw err;
|
|
3923
|
-
}
|
|
3924
|
-
};
|
|
3925
|
-
}
|
|
3926
3994
|
const sessionPush = new Command("push").description("Upload a locally-saved browser session (.ccqa/sessions/<profile>/<name>.json) to the hub, so it's available for `ccqa run` to fetch at run time. Encrypted at rest on the hub.").argument("<name>", "Session name to upload (resolves to .ccqa/sessions/<profile>/<name>.json)").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption$1).option(...profileOption$1).option("--cwd <path>", "Project root containing .ccqa/ (defaults to the current directory).").action(withHubErrors(async (rawName, opts) => {
|
|
3927
3995
|
const name = validateSessionName(rawName);
|
|
3928
3996
|
const cwd = resolveCwd(opts.cwd);
|
|
@@ -4084,22 +4152,6 @@ const pushCommand = new Command("push").description("Upload the report directory
|
|
|
4084
4152
|
info(`${resolveBaseUrl(opts)}/#/runs/${run.id}`);
|
|
4085
4153
|
}));
|
|
4086
4154
|
const hubCommand = new Command("hub").description("Client for a ccqa hub: push run results and manage sessions/variables/prompts used by `ccqa run`. See docs/hub.md.").addCommand(pushCommand).addCommand(sessionCommand$1).addCommand(varCommand).addCommand(promptCommand);
|
|
4087
|
-
/** Best-effort current branch: CI env vars first, then git, else null. */
|
|
4088
|
-
async function detectBranch(cwd) {
|
|
4089
|
-
const fromEnv = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME;
|
|
4090
|
-
if (fromEnv) return fromEnv;
|
|
4091
|
-
try {
|
|
4092
|
-
const { stdout } = await execFileP("git", [
|
|
4093
|
-
"rev-parse",
|
|
4094
|
-
"--abbrev-ref",
|
|
4095
|
-
"HEAD"
|
|
4096
|
-
], { cwd });
|
|
4097
|
-
const branch = stdout.trim();
|
|
4098
|
-
return branch && branch !== "HEAD" ? branch : null;
|
|
4099
|
-
} catch {
|
|
4100
|
-
return null;
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4103
4155
|
/** Loose check that a fetched session is agent-browser storage-state, mirroring loadStorageState. */
|
|
4104
4156
|
function isStorageStateShape(state) {
|
|
4105
4157
|
return typeof state === "object" && state !== null && Array.isArray(state.cookies) && Array.isArray(state.origins);
|
|
@@ -5206,7 +5258,8 @@ async function executeRun(targets, opts) {
|
|
|
5206
5258
|
project: projectForProfile,
|
|
5207
5259
|
cwd,
|
|
5208
5260
|
hubUrl: opts.hubUrl,
|
|
5209
|
-
hubToken: opts.hubToken
|
|
5261
|
+
hubToken: opts.hubToken,
|
|
5262
|
+
hubHeader: opts.hubHeader
|
|
5210
5263
|
});
|
|
5211
5264
|
} else await resolveProfileEnv({
|
|
5212
5265
|
profile: void 0,
|
|
@@ -5224,6 +5277,7 @@ async function executeRun(targets, opts) {
|
|
|
5224
5277
|
hubCtx = resolveHubContext({
|
|
5225
5278
|
hubUrl: opts.hubUrl,
|
|
5226
5279
|
hubToken: opts.hubToken,
|
|
5280
|
+
hubHeader: opts.hubHeader,
|
|
5227
5281
|
project: opts.project,
|
|
5228
5282
|
cwd
|
|
5229
5283
|
});
|
|
@@ -5573,6 +5627,7 @@ async function writeUnifiedReport(args) {
|
|
|
5573
5627
|
const { reportDir, results, diff, baseRef, customPromptVersion, opts } = args;
|
|
5574
5628
|
const data = {
|
|
5575
5629
|
schemaVersion: 1,
|
|
5630
|
+
kind: "run",
|
|
5576
5631
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5577
5632
|
runId: process.env["GITHUB_RUN_ID"] ?? null,
|
|
5578
5633
|
git: {
|
|
@@ -8584,7 +8639,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
8584
8639
|
project,
|
|
8585
8640
|
cwd: cwdForProfile,
|
|
8586
8641
|
hubUrl: opts.hubUrl,
|
|
8587
|
-
hubToken: opts.hubToken
|
|
8642
|
+
hubToken: opts.hubToken,
|
|
8643
|
+
hubHeader: opts.hubHeader
|
|
8588
8644
|
});
|
|
8589
8645
|
else await applyProfileFromOption({
|
|
8590
8646
|
profile: void 0,
|
|
@@ -8593,7 +8649,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
8593
8649
|
});
|
|
8594
8650
|
const hubClientForTrace = resolveHubClient({
|
|
8595
8651
|
hubUrl: opts.hubUrl,
|
|
8596
|
-
hubToken: opts.hubToken
|
|
8652
|
+
hubToken: opts.hubToken,
|
|
8653
|
+
hubHeader: opts.hubHeader
|
|
8597
8654
|
});
|
|
8598
8655
|
const hubContext = hubClientForTrace && project ? {
|
|
8599
8656
|
hub: hubClientForTrace,
|
|
@@ -9148,6 +9205,55 @@ function determineExitCode(results, threshold) {
|
|
|
9148
9205
|
}
|
|
9149
9206
|
return 0;
|
|
9150
9207
|
}
|
|
9208
|
+
/**
|
|
9209
|
+
* Spec-level status under the given threshold, mirroring determineExitCode's
|
|
9210
|
+
* per-issue logic (exit-code.ts) but scoped to a single SpecResult.
|
|
9211
|
+
*/
|
|
9212
|
+
function specStatus(result, threshold) {
|
|
9213
|
+
if (result.error) return "failed";
|
|
9214
|
+
for (const issue of result.issues) {
|
|
9215
|
+
if (issue.severity === "ERROR") return "failed";
|
|
9216
|
+
if (threshold === "warn" && issue.severity === "WARN") return "failed";
|
|
9217
|
+
}
|
|
9218
|
+
return "passed";
|
|
9219
|
+
}
|
|
9220
|
+
/**
|
|
9221
|
+
* Adapts `ccqa drift` results into the shared RunReportData shape so they can
|
|
9222
|
+
* be pushed to the hub (`ccqa drift --push`) and rendered by the same report
|
|
9223
|
+
* UI as `ccqa run`/`ccqa live`. Browser-execution fields (testCounts,
|
|
9224
|
+
* evidence, liveRun, ...) don't apply to a drift audit and are always null.
|
|
9225
|
+
*/
|
|
9226
|
+
function driftResultsToReport(results, meta) {
|
|
9227
|
+
const specResults = results.map((result) => ({
|
|
9228
|
+
feature: result.target.featureName,
|
|
9229
|
+
spec: result.target.specName,
|
|
9230
|
+
title: null,
|
|
9231
|
+
status: specStatus(result, meta.threshold),
|
|
9232
|
+
testCounts: null,
|
|
9233
|
+
durationMs: null,
|
|
9234
|
+
assertions: null,
|
|
9235
|
+
analysis: null,
|
|
9236
|
+
analysisSkipped: null,
|
|
9237
|
+
driftIssues: result.issues,
|
|
9238
|
+
failureLogExcerpt: null,
|
|
9239
|
+
diffExcerpt: null,
|
|
9240
|
+
specYaml: null,
|
|
9241
|
+
evidence: null,
|
|
9242
|
+
liveRun: null
|
|
9243
|
+
}));
|
|
9244
|
+
return {
|
|
9245
|
+
schemaVersion: 1,
|
|
9246
|
+
kind: "drift",
|
|
9247
|
+
createdAt: meta.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9248
|
+
runId: meta.runId ?? null,
|
|
9249
|
+
git: meta.git,
|
|
9250
|
+
model: meta.model ?? null,
|
|
9251
|
+
language: meta.language ?? null,
|
|
9252
|
+
promptVersion: meta.promptVersion ?? "1",
|
|
9253
|
+
customPromptVersion: null,
|
|
9254
|
+
results: specResults
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9151
9257
|
//#endregion
|
|
9152
9258
|
//#region src/drift/route-new-files.ts
|
|
9153
9259
|
/**
|
|
@@ -9263,7 +9369,7 @@ Return the spec keys that might be affected by any of the new files. Conservativ
|
|
|
9263
9369
|
//#endregion
|
|
9264
9370
|
//#region src/cli/drift.ts
|
|
9265
9371
|
const DEFAULT_CONCURRENCY = 3;
|
|
9266
|
-
const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.")).action(async (specPath, opts) => {
|
|
9372
|
+
const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption)).action(async (specPath, opts) => {
|
|
9267
9373
|
const format = parseFormat(opts.format);
|
|
9268
9374
|
const threshold = parseSeverity(opts.severity);
|
|
9269
9375
|
const concurrency = parseConcurrency(opts.concurrency);
|
|
@@ -9279,6 +9385,7 @@ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/s
|
|
|
9279
9385
|
header("drift", specPath ?? `${targets.length} spec${targets.length > 1 ? "s" : ""}`);
|
|
9280
9386
|
if (opts.cwd) meta("cwd", cwd);
|
|
9281
9387
|
}
|
|
9388
|
+
const baseRef = opts.changed ? resolveBaseRef(opts.base) : null;
|
|
9282
9389
|
if (opts.changed) {
|
|
9283
9390
|
const total = targets.length;
|
|
9284
9391
|
targets = await filterByChanged({
|
|
@@ -9304,8 +9411,69 @@ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/s
|
|
|
9304
9411
|
}
|
|
9305
9412
|
});
|
|
9306
9413
|
process.stdout.write(renderDrift(results, format, cwd));
|
|
9414
|
+
if (opts.push) await pushDriftResults({
|
|
9415
|
+
results,
|
|
9416
|
+
threshold,
|
|
9417
|
+
cwd,
|
|
9418
|
+
opts,
|
|
9419
|
+
format,
|
|
9420
|
+
baseRef
|
|
9421
|
+
});
|
|
9307
9422
|
process.exit(determineExitCode(results, threshold));
|
|
9308
9423
|
});
|
|
9424
|
+
/**
|
|
9425
|
+
* Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
|
|
9426
|
+
* shows up alongside `ccqa run` runs in the hub UI. Best-effort: a missing
|
|
9427
|
+
* hub connection warns and returns rather than failing the command (`--push`
|
|
9428
|
+
* never changes drift's own exit code).
|
|
9429
|
+
*
|
|
9430
|
+
* `resolveHub` is injectable so tests can supply a fake `HubClient` without
|
|
9431
|
+
* a real hub connection; it defaults to the real flag/env resolution.
|
|
9432
|
+
*/
|
|
9433
|
+
async function pushDriftResults(args, resolveHub = resolveHubClient) {
|
|
9434
|
+
const { results, threshold, cwd, opts, format, baseRef } = args;
|
|
9435
|
+
const hub = resolveHub(opts);
|
|
9436
|
+
if (!hub) {
|
|
9437
|
+
warn("--push requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN) — skipping push");
|
|
9438
|
+
return;
|
|
9439
|
+
}
|
|
9440
|
+
try {
|
|
9441
|
+
const project = resolveProject({
|
|
9442
|
+
project: opts.project,
|
|
9443
|
+
cwd
|
|
9444
|
+
});
|
|
9445
|
+
const [branch, head] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
|
|
9446
|
+
const report = driftResultsToReport(results, {
|
|
9447
|
+
threshold,
|
|
9448
|
+
git: {
|
|
9449
|
+
head,
|
|
9450
|
+
base: baseRef ?? null
|
|
9451
|
+
}
|
|
9452
|
+
});
|
|
9453
|
+
const dir = await mkdtemp(join(tmpdir(), "ccqa-drift-push-"));
|
|
9454
|
+
try {
|
|
9455
|
+
await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2), "utf8");
|
|
9456
|
+
const archive = await packDirToTarGz(dir);
|
|
9457
|
+
const run = await hub.pushRun(archive, {
|
|
9458
|
+
project,
|
|
9459
|
+
...branch ? { branch } : {},
|
|
9460
|
+
kind: "drift"
|
|
9461
|
+
});
|
|
9462
|
+
if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${run.id}`);
|
|
9463
|
+
} finally {
|
|
9464
|
+
await rm(dir, {
|
|
9465
|
+
recursive: true,
|
|
9466
|
+
force: true
|
|
9467
|
+
});
|
|
9468
|
+
}
|
|
9469
|
+
} catch (err) {
|
|
9470
|
+
if (err instanceof HubApiError) {
|
|
9471
|
+
error(`hub request failed (${err.status} ${err.code}): ${err.message}`);
|
|
9472
|
+
process.exit(2);
|
|
9473
|
+
}
|
|
9474
|
+
throw err;
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9309
9477
|
function exitWithNoSpecs(format, message) {
|
|
9310
9478
|
if (format === "json") process.stdout.write(`${JSON.stringify({ specs: [] }, null, 2)}\n`);
|
|
9311
9479
|
else if (format === "text") info(message);
|
|
@@ -10073,6 +10241,13 @@ z.object({
|
|
|
10073
10241
|
profile: z.string().nullable(),
|
|
10074
10242
|
branch: z.string().nullable(),
|
|
10075
10243
|
status: RunStatusSchema,
|
|
10244
|
+
kind: z.enum(["run", "drift"]).default("run"),
|
|
10245
|
+
drift: z.object({
|
|
10246
|
+
issues: z.number(),
|
|
10247
|
+
errors: z.number(),
|
|
10248
|
+
warnings: z.number(),
|
|
10249
|
+
specsWithIssues: z.number()
|
|
10250
|
+
}).nullable().default(null),
|
|
10076
10251
|
specs: z.object({
|
|
10077
10252
|
total: z.number(),
|
|
10078
10253
|
passed: z.number(),
|
|
@@ -10207,6 +10382,9 @@ function createPushRunHandler(config) {
|
|
|
10207
10382
|
const branch = requireBranch(ctx.url.searchParams.get("branch"));
|
|
10208
10383
|
const profileRaw = ctx.url.searchParams.get("profile");
|
|
10209
10384
|
const profile = profileRaw ? requireSafeSegment(profileRaw, "profile") : null;
|
|
10385
|
+
const kindRaw = ctx.url.searchParams.get("kind");
|
|
10386
|
+
if (kindRaw !== null && kindRaw !== "run" && kindRaw !== "drift") throw new HttpError(400, "invalid_param", `invalid kind: must be "run" or "drift"`);
|
|
10387
|
+
const kind = kindRaw ?? "run";
|
|
10210
10388
|
const body = await readBody(ctx.req, maxPushBytes);
|
|
10211
10389
|
const dir = await mkdtemp(join(tmpdir(), "ccqa-hub-push-"));
|
|
10212
10390
|
try {
|
|
@@ -10224,15 +10402,19 @@ function createPushRunHandler(config) {
|
|
|
10224
10402
|
const parsed = RunReportDataSchema.safeParse(reportJson);
|
|
10225
10403
|
if (!parsed.success) throw new HttpError(400, "invalid_report", `report.json is not a valid report: ${parsed.error.issues[0]?.message ?? "schema mismatch"}`);
|
|
10226
10404
|
const report = parsed.data;
|
|
10405
|
+
if (report.kind !== kind) throw new HttpError(400, "kind_mismatch", `?kind=${kind} does not match report.json's kind ("${report.kind}") — push with the matching ?kind= query param`);
|
|
10227
10406
|
const total = report.results.length;
|
|
10228
10407
|
const failed = report.results.filter((r) => r.status === "failed").length;
|
|
10229
10408
|
const status = failed > 0 ? "failed" : "passed";
|
|
10409
|
+
const drift = kind === "drift" ? summarizeDrift(report.results) : null;
|
|
10230
10410
|
const run = {
|
|
10231
10411
|
id: randomUUID(),
|
|
10232
10412
|
project,
|
|
10233
10413
|
profile,
|
|
10234
10414
|
branch,
|
|
10235
10415
|
status,
|
|
10416
|
+
kind,
|
|
10417
|
+
drift,
|
|
10236
10418
|
specs: {
|
|
10237
10419
|
total,
|
|
10238
10420
|
passed: total - failed,
|
|
@@ -10317,6 +10499,28 @@ async function getRunOr404(storage, id) {
|
|
|
10317
10499
|
if (!run) throw new HttpError(404, "not_found", `run "${id}" not found`);
|
|
10318
10500
|
return run;
|
|
10319
10501
|
}
|
|
10502
|
+
/** Tally `driftIssues` across all specs into the `Run.drift` summary counters. */
|
|
10503
|
+
function summarizeDrift(results) {
|
|
10504
|
+
let issues = 0;
|
|
10505
|
+
let errors = 0;
|
|
10506
|
+
let warnings = 0;
|
|
10507
|
+
let specsWithIssues = 0;
|
|
10508
|
+
for (const r of results) {
|
|
10509
|
+
const driftIssues = r.driftIssues ?? [];
|
|
10510
|
+
if (driftIssues.length > 0) specsWithIssues++;
|
|
10511
|
+
for (const issue of driftIssues) {
|
|
10512
|
+
issues++;
|
|
10513
|
+
if (issue.severity === "ERROR") errors++;
|
|
10514
|
+
else if (issue.severity === "WARN") warnings++;
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
return {
|
|
10518
|
+
issues,
|
|
10519
|
+
errors,
|
|
10520
|
+
warnings,
|
|
10521
|
+
specsWithIssues
|
|
10522
|
+
};
|
|
10523
|
+
}
|
|
10320
10524
|
/**
|
|
10321
10525
|
* A branch is a free-form label (e.g. `feature/foo`), stored verbatim and
|
|
10322
10526
|
* never used to build a filesystem path, so `/` is allowed — only length is
|
|
@@ -11095,7 +11299,7 @@ const HTML_BODY = `
|
|
|
11095
11299
|
|
|
11096
11300
|
<!-- Triage first: grading + learning is the most important action, so it
|
|
11097
11301
|
sits above the spec list rather than being buried below it. -->
|
|
11098
|
-
<div class="triage-head">
|
|
11302
|
+
<div class="triage-head" id="triage-head">
|
|
11099
11303
|
<h3 style="font-size:14px" data-i18n="detail.triage">Triage</h3>
|
|
11100
11304
|
<span class="triage-summary" id="triage-summary"></span>
|
|
11101
11305
|
</div>
|
|
@@ -11430,7 +11634,16 @@ const CSS = `
|
|
|
11430
11634
|
.badge-det { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
11431
11635
|
.badge.drift-warn { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
11432
11636
|
.badge.drift-warn .d { background: var(--amber); }
|
|
11637
|
+
.badge-drift { background: var(--violet-bg); color: var(--violet); border-color: var(--violet-border); }
|
|
11433
11638
|
.chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 6px; background: var(--surface-3); border: 1px solid var(--border); color: var(--fg-dim); font-size: 12px; font-family: var(--mono); }
|
|
11639
|
+
/* Below .chip in source order so these override its background/border/color
|
|
11640
|
+
when combined as class="chip drift-*-chip" (same specificity — source
|
|
11641
|
+
order decides). */
|
|
11642
|
+
.chip.kind-chip { color: var(--violet); background: var(--violet-bg); border-color: var(--violet-border); font-family: var(--font); margin-left: 6px; }
|
|
11643
|
+
.chip.drift-count-chip { color: var(--amber); background: var(--amber-bg); border-color: var(--amber-border); margin-left: 6px; }
|
|
11644
|
+
.chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); margin-left: 6px; }
|
|
11645
|
+
.drift-meta-box { display: flex; flex-direction: column; gap: 4px; }
|
|
11646
|
+
.drift-meta-chips { display: flex; gap: 6px; }
|
|
11434
11647
|
.specs { display: inline-flex; align-items: center; gap: 9px; }
|
|
11435
11648
|
.meter { width: 54px; height: 6px; border-radius: 3px; background: var(--fail-bg); overflow: hidden; }
|
|
11436
11649
|
.meter i { display: block; height: 100%; background: var(--pass); }
|
|
@@ -11519,6 +11732,7 @@ const CSS = `
|
|
|
11519
11732
|
.drift-step { font-family: var(--mono); font-size: 11px; color: var(--muted-2); }
|
|
11520
11733
|
.drift-msg { margin-top: 4px; color: var(--fg-dim); }
|
|
11521
11734
|
.drift-detail { margin-top: 3px; color: var(--muted); font-size: 12px; }
|
|
11735
|
+
.drift-clean { color: var(--pass); font-size: 13px; }
|
|
11522
11736
|
|
|
11523
11737
|
/* triage grading — an explicit question + a segmented single-select, framed
|
|
11524
11738
|
as an action ("tell us the real cause"), not a data readout. */
|
|
@@ -11724,12 +11938,17 @@ const CLIENT_JS = `
|
|
|
11724
11938
|
"detail.triage": "Triage",
|
|
11725
11939
|
"meta.branch": "Branch", "meta.specs": "Specs", "meta.prompt": "Prompt",
|
|
11726
11940
|
"meta.created": "Created", "meta.passed": "passed", "meta.profile": "Profile",
|
|
11941
|
+
"meta.drift": "Drift",
|
|
11727
11942
|
"rec.title": "Recommendation",
|
|
11728
11943
|
"acc.reasoning": "Reasoning", "acc.evidence": "Evidence", "acc.steps": "Live run steps",
|
|
11729
11944
|
"acc.assertions": "Assertions", "acc.drift": "Drift audit",
|
|
11730
11945
|
"acc.assertions.hint": "Test cases from the recorded spec run",
|
|
11731
11946
|
"spec.kind.live": "Live", "spec.kind.det": "Deterministic",
|
|
11732
11947
|
"spec.driftWarn": "Spec drift", "det.steps": "Steps",
|
|
11948
|
+
"kind.run": "Test run", "kind.drift": "Drift audit",
|
|
11949
|
+
"drift.summary.issues": "Issues", "drift.summary.errors": "Errors",
|
|
11950
|
+
"drift.summary.warnings": "Warnings", "drift.summary.specsWithIssues": "Specs with issues",
|
|
11951
|
+
"drift.clean": "No drift issues",
|
|
11733
11952
|
"grade.question": "What was the real cause?", "grade.predicted": "predicted",
|
|
11734
11953
|
"grade.ungraded": "ungraded", "grade.matches": "saved · matches",
|
|
11735
11954
|
"grade.corrected": "saved · corrected", "grade.saving": "saving…",
|
|
@@ -11785,12 +12004,17 @@ const CLIENT_JS = `
|
|
|
11785
12004
|
"detail.triage": "トリアージ",
|
|
11786
12005
|
"meta.branch": "ブランチ", "meta.specs": "スペック", "meta.prompt": "プロンプト",
|
|
11787
12006
|
"meta.created": "作成", "meta.passed": "合格", "meta.profile": "プロファイル",
|
|
12007
|
+
"meta.drift": "ドリフト",
|
|
11788
12008
|
"rec.title": "推奨対応",
|
|
11789
12009
|
"acc.reasoning": "推論", "acc.evidence": "根拠", "acc.steps": "実行ステップ",
|
|
11790
12010
|
"acc.assertions": "アサーション", "acc.drift": "ドリフト監査",
|
|
11791
12011
|
"acc.assertions.hint": "記録したスペック実行のテストケース",
|
|
11792
12012
|
"spec.kind.live": "ライブ", "spec.kind.det": "決定的",
|
|
11793
12013
|
"spec.driftWarn": "仕様ドリフト", "det.steps": "ステップ",
|
|
12014
|
+
"kind.run": "テスト実行", "kind.drift": "ドリフト監査",
|
|
12015
|
+
"drift.summary.issues": "問題数", "drift.summary.errors": "エラー",
|
|
12016
|
+
"drift.summary.warnings": "警告", "drift.summary.specsWithIssues": "問題のあるスペック",
|
|
12017
|
+
"drift.clean": "ドリフトの問題なし",
|
|
11794
12018
|
"grade.question": "実際の原因は何でしたか?", "grade.predicted": "予測",
|
|
11795
12019
|
"grade.ungraded": "未評価", "grade.matches": "保存済み · 一致",
|
|
11796
12020
|
"grade.corrected": "保存済み · 修正", "grade.saving": "保存中…",
|
|
@@ -12170,6 +12394,19 @@ const CLIENT_JS = `
|
|
|
12170
12394
|
runCell.appendChild(el("div", "runid", r.id.slice(0, 8)));
|
|
12171
12395
|
var sub = el("div", "subline");
|
|
12172
12396
|
sub.appendChild(ciBadge(r));
|
|
12397
|
+
if (r.kind === "drift") {
|
|
12398
|
+
sub.appendChild(el("span", "chip kind-chip", t("kind.drift")));
|
|
12399
|
+
if (r.drift) {
|
|
12400
|
+
if (r.drift.errors > 0) {
|
|
12401
|
+
sub.appendChild(el("span", "chip drift-errors-chip", t("drift.summary.errors") + " " + r.drift.errors));
|
|
12402
|
+
}
|
|
12403
|
+
if (r.drift.warnings > 0) {
|
|
12404
|
+
sub.appendChild(el("span", "chip drift-count-chip", t("drift.summary.warnings") + " " + r.drift.warnings));
|
|
12405
|
+
}
|
|
12406
|
+
}
|
|
12407
|
+
} else {
|
|
12408
|
+
sub.appendChild(el("span", "chip kind-chip", t("kind.run")));
|
|
12409
|
+
}
|
|
12173
12410
|
runCell.appendChild(sub);
|
|
12174
12411
|
tr.appendChild(runCell);
|
|
12175
12412
|
|
|
@@ -12245,7 +12482,29 @@ const CLIENT_JS = `
|
|
|
12245
12482
|
metaItem(t("meta.branch"), branchChip);
|
|
12246
12483
|
// Which environment the run executed against (recorded at push, display-only).
|
|
12247
12484
|
if (run.profile) metaItem(t("meta.profile"), el("span", "chip", run.profile));
|
|
12248
|
-
|
|
12485
|
+
if (run.kind === "drift" && run.drift) {
|
|
12486
|
+
// Drift runs have no live/deterministic spec pass count. Errors/warnings
|
|
12487
|
+
// are the actionable counts and get colored chips; issues/specsWithIssues
|
|
12488
|
+
// are supplementary totals shown as a muted line below.
|
|
12489
|
+
if (run.drift.errors === 0 && run.drift.warnings === 0) {
|
|
12490
|
+
metaItem(t("meta.drift"), el("div", "drift-clean", t("drift.clean")));
|
|
12491
|
+
} else {
|
|
12492
|
+
var driftBox = el("div", "drift-meta-box");
|
|
12493
|
+
var chips = el("div", "drift-meta-chips");
|
|
12494
|
+
if (run.drift.errors > 0) {
|
|
12495
|
+
chips.appendChild(el("span", "chip drift-errors-chip", t("drift.summary.errors") + " " + run.drift.errors));
|
|
12496
|
+
}
|
|
12497
|
+
if (run.drift.warnings > 0) {
|
|
12498
|
+
chips.appendChild(el("span", "chip drift-count-chip", t("drift.summary.warnings") + " " + run.drift.warnings));
|
|
12499
|
+
}
|
|
12500
|
+
driftBox.appendChild(chips);
|
|
12501
|
+
var driftSub = el("div", "muted", t("drift.summary.specsWithIssues") + " " + run.drift.specsWithIssues + " / " + t("drift.summary.issues") + " " + run.drift.issues);
|
|
12502
|
+
driftBox.appendChild(driftSub);
|
|
12503
|
+
metaItem(t("meta.drift"), driftBox);
|
|
12504
|
+
}
|
|
12505
|
+
} else {
|
|
12506
|
+
metaItem(t("meta.specs"), run.specs.passed + " / " + run.specs.total + " " + t("meta.passed"));
|
|
12507
|
+
}
|
|
12249
12508
|
metaItem(t("meta.prompt"), run.promptVersion || "—");
|
|
12250
12509
|
metaItem(t("meta.created"), relTime(run.createdAt));
|
|
12251
12510
|
head.appendChild(meta);
|
|
@@ -12583,7 +12842,7 @@ const CLIENT_JS = `
|
|
|
12583
12842
|
return wrap;
|
|
12584
12843
|
}
|
|
12585
12844
|
|
|
12586
|
-
function renderSpecCard(runId, r, triageState) {
|
|
12845
|
+
function renderSpecCard(runId, r, triageState, isDrift) {
|
|
12587
12846
|
var card = el("div", "spec-card " + r.status); // .passed / .failed rail
|
|
12588
12847
|
var head = el("div", "spec-card-head");
|
|
12589
12848
|
var nameBlock = el("div");
|
|
@@ -12591,7 +12850,10 @@ const CLIENT_JS = `
|
|
|
12591
12850
|
nameBlock.appendChild(el("div", "slug", r.feature + " / " + r.spec));
|
|
12592
12851
|
head.appendChild(nameBlock);
|
|
12593
12852
|
head.appendChild(el("div", "spacer"));
|
|
12594
|
-
head.appendChild(
|
|
12853
|
+
head.appendChild(
|
|
12854
|
+
isDrift ? el("span", "badge badge-drift", t("kind.drift"))
|
|
12855
|
+
: (r.liveRun ? el("span", "badge-live", t("spec.kind.live"))
|
|
12856
|
+
: el("span", "badge-det", t("spec.kind.det"))));
|
|
12595
12857
|
head.appendChild(statusBadge(r.status));
|
|
12596
12858
|
var hasDriftError = r.driftIssues && r.driftIssues.some(function (d) { return d.severity === "ERROR"; });
|
|
12597
12859
|
if (hasDriftError) {
|
|
@@ -12605,6 +12867,11 @@ const CLIENT_JS = `
|
|
|
12605
12867
|
var body = el("div", "spec-card-body");
|
|
12606
12868
|
var any = false;
|
|
12607
12869
|
|
|
12870
|
+
if (isDrift && (!r.driftIssues || r.driftIssues.length === 0)) {
|
|
12871
|
+
body.appendChild(el("div", "drift-clean", t("drift.clean")));
|
|
12872
|
+
any = true;
|
|
12873
|
+
}
|
|
12874
|
+
|
|
12608
12875
|
if (r.status === "failed" && r.analysis) {
|
|
12609
12876
|
// Tier2: the verdict block (analysis) + the grading action, always shown.
|
|
12610
12877
|
body.appendChild(analysisSection(runId, r));
|
|
@@ -12651,10 +12918,10 @@ const CLIENT_JS = `
|
|
|
12651
12918
|
return card;
|
|
12652
12919
|
}
|
|
12653
12920
|
|
|
12654
|
-
function renderSpecCards(runId, results, triageState) {
|
|
12921
|
+
function renderSpecCards(runId, results, triageState, isDrift) {
|
|
12655
12922
|
var container = document.getElementById("spec-cards");
|
|
12656
12923
|
clear(container);
|
|
12657
|
-
results.forEach(function (r) { container.appendChild(renderSpecCard(runId, r, triageState)); });
|
|
12924
|
+
results.forEach(function (r) { container.appendChild(renderSpecCard(runId, r, triageState, isDrift)); });
|
|
12658
12925
|
document.getElementById("detail-spec-count").textContent = "· " + results.length;
|
|
12659
12926
|
}
|
|
12660
12927
|
|
|
@@ -12791,9 +13058,14 @@ const CLIENT_JS = `
|
|
|
12791
13058
|
// triage loads so the saved grades restore. Keeping these separate means
|
|
12792
13059
|
// a triage failure (or a throw while rendering cards) can't be mislabelled
|
|
12793
13060
|
// as the other, and neither escapes its own catch.
|
|
12794
|
-
|
|
13061
|
+
var isDrift = report.kind === "drift";
|
|
13062
|
+
document.getElementById("triage-head").hidden = isDrift;
|
|
13063
|
+
document.getElementById("matrix-card").hidden = isDrift;
|
|
13064
|
+
document.getElementById("triage-progress").hidden = isDrift;
|
|
13065
|
+
renderSpecCards(runId, report.results, { byKey: {} }, isDrift);
|
|
13066
|
+
if (isDrift) return; // drift runs have no triage: skip loadTriage entirely
|
|
12795
13067
|
loadTriage(runId, function (triageState) {
|
|
12796
|
-
renderSpecCards(runId, report.results, triageState);
|
|
13068
|
+
renderSpecCards(runId, report.results, triageState, isDrift);
|
|
12797
13069
|
});
|
|
12798
13070
|
}).catch(detailError("Error loading report"));
|
|
12799
13071
|
}
|
|
@@ -31,6 +31,16 @@ declare const RunSchema: z.ZodObject<{
|
|
|
31
31
|
passed: "passed";
|
|
32
32
|
failed: "failed";
|
|
33
33
|
}>;
|
|
34
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
35
|
+
run: "run";
|
|
36
|
+
drift: "drift";
|
|
37
|
+
}>>;
|
|
38
|
+
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
39
|
+
issues: z.ZodNumber;
|
|
40
|
+
errors: z.ZodNumber;
|
|
41
|
+
warnings: z.ZodNumber;
|
|
42
|
+
specsWithIssues: z.ZodNumber;
|
|
43
|
+
}, z.core.$strip>>>;
|
|
34
44
|
specs: z.ZodObject<{
|
|
35
45
|
total: z.ZodNumber;
|
|
36
46
|
passed: z.ZodNumber;
|
|
@@ -171,6 +181,12 @@ type LabelsExport = z.infer<typeof LabelsExportSchema>;
|
|
|
171
181
|
interface HubClientOptions {
|
|
172
182
|
baseUrl: string;
|
|
173
183
|
token: string;
|
|
184
|
+
/**
|
|
185
|
+
* Extra headers sent on every request, ahead of any per-call headers and
|
|
186
|
+
* the `Authorization` header (e.g. an infra gateway/ALB bypass header in
|
|
187
|
+
* CI — never overrides `Authorization`).
|
|
188
|
+
*/
|
|
189
|
+
headers?: Record<string, string>;
|
|
174
190
|
/** Override for testing; defaults to the global `fetch`. */
|
|
175
191
|
fetchImpl?: typeof fetch;
|
|
176
192
|
}
|
|
@@ -197,6 +213,7 @@ interface HubClient {
|
|
|
197
213
|
project: string;
|
|
198
214
|
branch?: string;
|
|
199
215
|
profile?: string;
|
|
216
|
+
kind?: "run" | "drift";
|
|
200
217
|
}): Promise<Run>;
|
|
201
218
|
listRuns(q?: {
|
|
202
219
|
project?: string;
|
|
@@ -51,6 +51,7 @@ function createHubClient(opts) {
|
|
|
51
51
|
...init,
|
|
52
52
|
signal,
|
|
53
53
|
headers: {
|
|
54
|
+
...opts.headers,
|
|
54
55
|
...init.headers,
|
|
55
56
|
Authorization: `Bearer ${opts.token}`
|
|
56
57
|
}
|
|
@@ -96,6 +97,7 @@ function createHubClient(opts) {
|
|
|
96
97
|
const params = new URLSearchParams({ project: meta.project });
|
|
97
98
|
if (meta.branch) params.set("branch", meta.branch);
|
|
98
99
|
if (meta.profile) params.set("profile", meta.profile);
|
|
100
|
+
if (meta.kind) params.set("kind", meta.kind);
|
|
99
101
|
return json(`/api/v1/runs?${params}`, {
|
|
100
102
|
method: "POST",
|
|
101
103
|
headers: { "Content-Type": "application/gzip" },
|
package/dist/package.json
CHANGED