@kici-dev/compiler 0.1.16 → 0.1.17
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/auth/headless-detect.js +1 -1
- package/dist/cli-banner.js +1 -1
- package/dist/cli.js +56 -19
- package/dist/commands/approve.js +1 -1
- package/dist/commands/compile.js +5 -3
- package/dist/commands/diagnostics.d.ts +7 -0
- package/dist/commands/diagnostics.js +58 -0
- package/dist/commands/docs.js +1 -1
- package/dist/commands/drain-worker.js +1 -1
- package/dist/commands/endpoints.js +1 -1
- package/dist/commands/fixture.js +1 -1
- package/dist/commands/held-run-client.js +1 -1
- package/dist/commands/held-run-resolve.js +1 -1
- package/dist/commands/hook.js +1 -1
- package/dist/commands/index.d.ts +16 -4
- package/dist/commands/index.js +10 -4
- package/dist/commands/init.js +2 -2
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +1 -3
- package/dist/commands/logout.js +1 -1
- package/dist/commands/orchestrators.d.ts +25 -0
- package/dist/commands/orchestrators.js +115 -0
- package/dist/commands/org.js +1 -1
- package/dist/commands/reject.js +1 -1
- package/dist/commands/run.d.ts +2 -9
- package/dist/commands/run.js +256 -315
- package/dist/commands/runs/cancel.d.ts +6 -0
- package/dist/commands/runs/cancel.js +32 -0
- package/dist/commands/runs/list.d.ts +6 -0
- package/dist/commands/runs/list.js +49 -0
- package/dist/commands/runs/logs.d.ts +7 -0
- package/dist/commands/runs/logs.js +67 -0
- package/dist/commands/runs/rerun.d.ts +5 -0
- package/dist/commands/runs/rerun.js +21 -0
- package/dist/commands/runs/show.d.ts +5 -0
- package/dist/commands/runs/show.js +65 -0
- package/dist/commands/secrets-list.d.ts +7 -9
- package/dist/commands/secrets-list.js +40 -64
- package/dist/commands/test.d.ts +4 -0
- package/dist/commands/test.js +1 -1
- package/dist/commands/types.d.ts +3 -5
- package/dist/commands/types.js +13 -37
- package/dist/commands/verify-attestation.d.ts +12 -0
- package/dist/commands/verify-attestation.js +69 -0
- package/dist/commands/watch.js +1 -1
- package/dist/commands/workflows.js +14 -34
- package/dist/errors/capability-gap.js +1 -1
- package/dist/errors/formatter.js +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/execution/executor.js +1 -1
- package/dist/execution/index.js +1 -1
- package/dist/execution/sdk-alias.js +1 -1
- package/dist/execution/ts-loader.js +1 -1
- package/dist/fixtures/compiler.js +1 -1
- package/dist/fixtures/defaults/index.js +1 -1
- package/dist/format.js +1 -1
- package/dist/generators/secrets-dts.js +2 -2
- package/dist/hooks/detector.js +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/installer.js +1 -1
- package/dist/hooks/templates.js +1 -1
- package/dist/index.js +1 -1
- package/dist/llm-context/llms-full.txt +1047 -132
- package/dist/llm-context/llms.txt +4 -1
- package/dist/local-executor/dag-scheduler.js +1 -1
- package/dist/local-executor/index.js +1 -1
- package/dist/local-executor/job-runner.js +37 -16
- package/dist/local-executor/materializer.js +1 -1
- package/dist/local-executor/output-streamer.js +1 -1
- package/dist/local-executor/payload-generator.js +1 -1
- package/dist/local-executor/picker.js +1 -1
- package/dist/local-executor/secret-loader.js +1 -1
- package/dist/local-executor/to-event-payload.js +1 -1
- package/dist/local-executor/types.js +1 -1
- package/dist/local-executor/workflow-lock.js +0 -0
- package/dist/lockfile/generator.d.ts +7 -5
- package/dist/lockfile/generator.js +21 -18
- package/dist/lockfile/hash-files.js +1 -1
- package/dist/lockfile/hasher.js +1 -1
- package/dist/lockfile/index.js +1 -1
- package/dist/lockfile/purity-analyzer.js +1 -1
- package/dist/postinstall.js +1 -1
- package/dist/provenance-trust-root.d.ts +14 -0
- package/dist/provenance-trust-root.js +49 -0
- package/dist/remote/config.d.ts +6 -2
- package/dist/remote/config.js +7 -4
- package/dist/remote/dashboard-client.d.ts +92 -0
- package/dist/remote/dashboard-client.js +168 -0
- package/dist/remote/encryption.js +3 -4
- package/dist/remote/history.js +1 -1
- package/dist/remote/oauth.js +1 -1
- package/dist/remote/oidc-discovery.js +1 -1
- package/dist/remote/output/json.js +1 -1
- package/dist/remote/output/junit.js +1 -1
- package/dist/remote/output/streaming.js +1 -1
- package/dist/remote/output/summary.js +1 -1
- package/dist/remote/platform-client.d.ts +142 -0
- package/dist/remote/platform-client.js +187 -0
- package/dist/remote/prod-defaults.js +1 -1
- package/dist/remote/render.d.ts +11 -0
- package/dist/remote/render.js +48 -0
- package/dist/remote/secret-upload.js +1 -1
- package/dist/remote/uploader.d.ts +9 -0
- package/dist/remote/uploader.js +51 -4
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +8 -3
- package/dist/templates/index.js +1 -1
- package/dist/templates/package-json.js +4 -5
- package/dist/templates/tsconfig-json.js +1 -1
- package/dist/templates/workflows/hello-world.js +2 -2
- package/dist/templates/workflows/hello-world.ts +1 -1
- package/dist/templates/workflows/pr-checks.js +3 -3
- package/dist/templates/workflows/pr-checks.ts +2 -2
- package/dist/test-runner/dry-run.js +1 -1
- package/dist/test-runner/event-types.js +1 -1
- package/dist/test-runner/git-detector.js +1 -1
- package/dist/test-runner/index.js +1 -1
- package/dist/test-runner/job-executor.js +1 -1
- package/dist/test-runner/output-formatter.js +1 -1
- package/dist/test-runner/payload-builder.js +1 -1
- package/dist/test-runner/rule-evaluator.js +1 -1
- package/dist/test-runner/secrets-file.js +1 -1
- package/dist/test-runner/step-context.js +10 -6
- package/dist/types.d.ts +8 -5
- package/dist/types.js +2 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/validator.js +1 -1
- package/dist/workflows/hello-world.ts +1 -1
- package/dist/workflows/pr-checks.ts +2 -2
- package/package.json +5 -4
- package/sbom.spdx.json +68 -38
- package/dist/commands/cancel.js +0 -124
- package/dist/commands/status.js +0 -212
- package/dist/remote/client.js +0 -203
- package/dist/remote/observer.js +0 -174
- /package/dist/{chunk-gOLHoazu.js → chunk-BTugEXQM.js} +0 -0
package/dist/commands/run.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import "../chunk-
|
|
1
|
+
import "../chunk-BTugEXQM.js";
|
|
2
2
|
import { resolveKiciDir } from "../execution/executor.js";
|
|
3
3
|
import "../execution/index.js";
|
|
4
|
-
import { AuthenticationError, ConnectionError, OrchestratorClient } from "../remote/client.js";
|
|
5
4
|
import { loadGlobalConfig } from "../remote/config.js";
|
|
6
5
|
import { RunHistory } from "../remote/history.js";
|
|
7
|
-
import {
|
|
6
|
+
import { AccessDeniedError, AmbiguousClusterError, AuthenticationError, ConnectionError, NoClusterError, PlatformRunClient } from "../remote/platform-client.js";
|
|
8
7
|
import { buildEncryptedSecrets } from "../remote/secret-upload.js";
|
|
9
8
|
import { createOverlayTarball, getSizeWarning, uploadTarball } from "../remote/uploader.js";
|
|
10
9
|
import { formatJsonResult } from "../remote/output/json.js";
|
|
11
10
|
import { formatJunitResult } from "../remote/output/junit.js";
|
|
12
|
-
import { StreamingFormatter } from "../remote/output/streaming.js";
|
|
13
11
|
import { formatErrorHighlight, formatMultiFixtureSummary, formatSummary } from "../remote/output/summary.js";
|
|
14
12
|
import { compileFixtures, filterFixtures } from "../fixtures/compiler.js";
|
|
15
13
|
import path from "node:path";
|
|
@@ -17,6 +15,15 @@ import pc from "picocolors";
|
|
|
17
15
|
import { readFile, writeFile } from "node:fs/promises";
|
|
18
16
|
import { formatBytes, logger, toErrorMessage } from "@kici-dev/core";
|
|
19
17
|
//#region src/commands/run.ts
|
|
18
|
+
/** Terminal run statuses returned by the Platform run-status snapshot. */
|
|
19
|
+
const TERMINAL_STATUSES = new Set([
|
|
20
|
+
"success",
|
|
21
|
+
"failed",
|
|
22
|
+
"cancelled",
|
|
23
|
+
"error"
|
|
24
|
+
]);
|
|
25
|
+
/** Interval between status/log polls while a run is active. */
|
|
26
|
+
const POLL_INTERVAL_MS = 750;
|
|
20
27
|
/**
|
|
21
28
|
* Run a workflow locally using the local executor.
|
|
22
29
|
* Thin wrapper that delegates to executeLocal from local-executor.
|
|
@@ -43,8 +50,7 @@ async function runLocalCommand(options) {
|
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
/**
|
|
46
|
-
* Run fixtures remotely
|
|
47
|
-
* Migrated from test.ts — full remote execution functionality.
|
|
53
|
+
* Run fixtures remotely, routed through the Platform.
|
|
48
54
|
*/
|
|
49
55
|
async function runRemoteCommand(fixture, options) {
|
|
50
56
|
if (options.debug) {
|
|
@@ -116,33 +122,59 @@ function describeEvent(event) {
|
|
|
116
122
|
return "custom";
|
|
117
123
|
}
|
|
118
124
|
/**
|
|
119
|
-
*
|
|
125
|
+
* Resolve the authenticated Platform client and the run target (org + cluster).
|
|
126
|
+
*
|
|
127
|
+
* Org resolution: `--org` → `config.activeOrgId` → error.
|
|
128
|
+
* Cluster resolution: `--orchestrator` → `config.defaultClusters[orgId]` →
|
|
129
|
+
* omit and let the Platform sole-select (or return a 422 the CLI surfaces).
|
|
120
130
|
*/
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
124
|
-
logger.error(pc.red("Not authenticated. Run `kici login` to
|
|
125
|
-
return
|
|
131
|
+
function resolvePlatformContext(config, options) {
|
|
132
|
+
const token = config.pat;
|
|
133
|
+
if (!token) {
|
|
134
|
+
logger.error(pc.red("Not authenticated. Run `kici login` to authenticate."));
|
|
135
|
+
return null;
|
|
126
136
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
if (!config.platformEndpoint) {
|
|
138
|
+
logger.error(pc.red("No Platform endpoint configured. Run `kici login` to set up your Platform."));
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const orgId = options.org ?? config.activeOrgId;
|
|
142
|
+
if (!orgId) {
|
|
143
|
+
logger.error(pc.red("No target organization. Select one with `kici org use <org>` or pass `--org <id>`."));
|
|
144
|
+
return null;
|
|
131
145
|
}
|
|
146
|
+
const orchestrator = options.orchestrator;
|
|
147
|
+
const defaultCluster = config.defaultClusters?.[orgId];
|
|
148
|
+
return {
|
|
149
|
+
client: new PlatformRunClient({
|
|
150
|
+
platformEndpoint: config.platformEndpoint,
|
|
151
|
+
token
|
|
152
|
+
}),
|
|
153
|
+
orgId,
|
|
154
|
+
target: {
|
|
155
|
+
orchestrator,
|
|
156
|
+
defaultCluster
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Run fixtures remotely against the Platform.
|
|
162
|
+
*/
|
|
163
|
+
async function runFixturesRemotely(fixtures, options) {
|
|
164
|
+
const config = await loadGlobalConfig();
|
|
165
|
+
const ctx = resolvePlatformContext(config, options);
|
|
166
|
+
if (!ctx) return false;
|
|
132
167
|
if (options.json) options.quiet = true;
|
|
133
|
-
const client = new OrchestratorClient({
|
|
134
|
-
endpoint,
|
|
135
|
-
token: config.token
|
|
136
|
-
});
|
|
137
168
|
const history = new RunHistory();
|
|
138
169
|
await history.load();
|
|
139
170
|
if (!options.quiet) {
|
|
140
|
-
logger.info(pc.gray(`
|
|
171
|
+
logger.info(pc.gray(`Platform: ${config.platformEndpoint}`));
|
|
172
|
+
logger.info(pc.gray(`Organization: ${ctx.orgId}`));
|
|
141
173
|
logger.info(pc.gray(`Fixtures: ${fixtures.length} to run\n`));
|
|
142
174
|
}
|
|
143
175
|
const results = [];
|
|
144
176
|
if (options.parallel && fixtures.length > 1) {
|
|
145
|
-
const promises = fixtures.map((f) => runSingleFixture(f,
|
|
177
|
+
const promises = fixtures.map((f) => runSingleFixture(f, ctx, options, config, history));
|
|
146
178
|
const settled = await Promise.allSettled(promises);
|
|
147
179
|
for (const s of settled) if (s.status === "fulfilled") results.push(s.value);
|
|
148
180
|
else results.push({
|
|
@@ -152,10 +184,16 @@ async function runFixturesRemotely(fixtures, options) {
|
|
|
152
184
|
reason: s.reason instanceof Error ? s.reason.message : String(s.reason)
|
|
153
185
|
});
|
|
154
186
|
} else for (const f of fixtures) {
|
|
155
|
-
const result = await runSingleFixture(f,
|
|
187
|
+
const result = await runSingleFixture(f, ctx, options, config, history);
|
|
156
188
|
results.push(result);
|
|
157
189
|
if (result.status === "failed" || result.status === "error") break;
|
|
158
190
|
}
|
|
191
|
+
return renderResults(results, options);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Render the aggregated fixture results in the requested format.
|
|
195
|
+
*/
|
|
196
|
+
async function renderResults(results, options) {
|
|
159
197
|
const runResults = results.filter((r) => r.status === "success" || r.status === "failed" || r.status === "cancelled").map((r) => ({
|
|
160
198
|
fixtureId: r.fixtureId,
|
|
161
199
|
runId: r.runId,
|
|
@@ -173,44 +211,69 @@ async function runFixturesRemotely(fixtures, options) {
|
|
|
173
211
|
return results.every((r) => r.status === "accepted" || r.status === "success");
|
|
174
212
|
}
|
|
175
213
|
/**
|
|
176
|
-
*
|
|
214
|
+
* Prepare the overlay tarball + routing metadata for a run.
|
|
215
|
+
*
|
|
216
|
+
* The overlay always carries the full local working tree (the run executes the
|
|
217
|
+
* developer's local code, never a clone). The compiled lock is inlined so the
|
|
218
|
+
* orchestrator can match triggers under the `remote:<orgId>` anchor without a
|
|
219
|
+
* webhook provider to fetch it from a git host.
|
|
220
|
+
*/
|
|
221
|
+
async function prepareOverlay(options) {
|
|
222
|
+
const kiciDir = resolveKiciDir(options.kiciDir);
|
|
223
|
+
const repoRoot = path.resolve(kiciDir, "..");
|
|
224
|
+
if (!options.quiet) logger.info(pc.gray("Creating overlay tarball..."));
|
|
225
|
+
const { tarballPath, summary, hasRemote } = await createOverlayTarball(repoRoot, { fullWorkingTree: true });
|
|
226
|
+
if (!options.quiet) logger.info(pc.gray("Running your local working tree (overlay includes .git, so git steps work)"));
|
|
227
|
+
const inlineLockFile = await readFile(path.join(kiciDir, "kici.lock.json"), "utf-8");
|
|
228
|
+
if (!options.quiet) {
|
|
229
|
+
logger.info(pc.gray(`${summary.fileCount} files changed, ${summary.newFiles} new, ${summary.deletedFiles} deleted (${formatBytes(summary.compressedSize)} compressed)`));
|
|
230
|
+
const sizeWarning = getSizeWarning(summary.compressedSize);
|
|
231
|
+
if (sizeWarning) logger.info(pc.yellow(sizeWarning));
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
tarballPath,
|
|
235
|
+
summary,
|
|
236
|
+
hasRemote,
|
|
237
|
+
inlineLockFile,
|
|
238
|
+
kiciDir,
|
|
239
|
+
repoRoot
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Init the upload (Platform) and PUT the encrypted tarball directly to the
|
|
244
|
+
* object store (data plane). Returns the upload id + the encryption keys.
|
|
177
245
|
*/
|
|
178
|
-
async function
|
|
246
|
+
async function initAndUpload(ctx, overlay, options) {
|
|
247
|
+
if (!options.quiet) logger.info(pc.gray("Initializing upload..."));
|
|
248
|
+
const upload = await ctx.client.initUpload(ctx.orgId, ctx.target, {
|
|
249
|
+
sha: overlay.summary.sha,
|
|
250
|
+
fileCount: overlay.summary.fileCount,
|
|
251
|
+
compressedSize: overlay.summary.compressedSize
|
|
252
|
+
});
|
|
253
|
+
if (!options.quiet) logger.info(pc.gray("Uploading overlay..."));
|
|
254
|
+
const uploadResult = await uploadTarball({
|
|
255
|
+
tarballPath: overlay.tarballPath,
|
|
256
|
+
signedUrl: upload.signedUrl,
|
|
257
|
+
orchestratorPublicKey: Buffer.from(upload.publicKey, "base64")
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
uploadId: upload.uploadId,
|
|
261
|
+
publicKey: upload.publicKey,
|
|
262
|
+
cliPublicKey: uploadResult.cliPublicKey.toString("base64")
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Run a single fixture remotely through the Platform.
|
|
267
|
+
*/
|
|
268
|
+
async function runSingleFixture(fixture, ctx, options, config, history) {
|
|
179
269
|
const opts = typeof fixture.fixture.options === "function" ? await fixture.fixture.options() : fixture.fixture.options;
|
|
180
270
|
if (!options.quiet) logger.info(pc.cyan(`\n--- ${fixture.id} ---`));
|
|
181
271
|
try {
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
if (!options.quiet) logger.info(pc.gray("Creating overlay tarball..."));
|
|
185
|
-
const { tarballPath, summary, hasRemote } = await createOverlayTarball(repoRoot);
|
|
186
|
-
let routingKey = options.routingKey ?? config.routingKey ?? "default";
|
|
187
|
-
if (!hasRemote) {
|
|
188
|
-
routingKey = `local:${path.basename(repoRoot)}`;
|
|
189
|
-
if (!options.quiet) logger.warn("No remote detected -- steps that use git commands will fail (no .git directory)");
|
|
190
|
-
}
|
|
191
|
-
let inlineLockFile;
|
|
192
|
-
if (!hasRemote) inlineLockFile = await readFile(path.join(kiciDir, "kici.lock.json"), "utf-8");
|
|
193
|
-
if (!options.quiet) {
|
|
194
|
-
logger.info(pc.gray(`${summary.fileCount} files changed, ${summary.newFiles} new, ${summary.deletedFiles} deleted (${formatBytes(summary.compressedSize)} compressed)`));
|
|
195
|
-
const sizeWarning = getSizeWarning(summary.compressedSize);
|
|
196
|
-
if (sizeWarning) logger.info(pc.yellow(sizeWarning));
|
|
197
|
-
}
|
|
198
|
-
if (!options.quiet) logger.info(pc.gray("Initializing upload..."));
|
|
199
|
-
const upload = await client.initUpload({
|
|
200
|
-
routingKey,
|
|
201
|
-
sha: summary.sha,
|
|
202
|
-
fileCount: summary.fileCount,
|
|
203
|
-
compressedSize: summary.compressedSize
|
|
204
|
-
});
|
|
205
|
-
if (!options.quiet) logger.info(pc.gray("Uploading overlay..."));
|
|
206
|
-
const uploadResult = await uploadTarball({
|
|
207
|
-
tarballPath,
|
|
208
|
-
signedUrl: upload.signedUrl,
|
|
209
|
-
orchestratorPublicKey: Buffer.from(upload.publicKey, "base64")
|
|
210
|
-
});
|
|
272
|
+
const overlay = await prepareOverlay(options);
|
|
273
|
+
const uploaded = await initAndUpload(ctx, overlay, options);
|
|
211
274
|
const event = buildEventFromFixture(opts);
|
|
212
|
-
|
|
213
|
-
const repoName = path.basename(repoRoot);
|
|
275
|
+
{
|
|
276
|
+
const repoName = path.basename(overlay.repoRoot);
|
|
214
277
|
const repo = event.payload.repository;
|
|
215
278
|
event.payload.repository = {
|
|
216
279
|
...repo ?? {},
|
|
@@ -218,21 +281,20 @@ async function runSingleFixture(fixture, client, options, config, history) {
|
|
|
218
281
|
};
|
|
219
282
|
}
|
|
220
283
|
if (!options.quiet) logger.info(pc.gray("Triggering test run..."));
|
|
221
|
-
const encrypted = await buildEncryptedSecrets(kiciDir, options.envFlags, options.context,
|
|
222
|
-
const triggerResult = await client.
|
|
284
|
+
const encrypted = await buildEncryptedSecrets(overlay.kiciDir, options.envFlags, options.context, uploaded.publicKey);
|
|
285
|
+
const triggerResult = await ctx.client.trigger(ctx.orgId, ctx.target, {
|
|
223
286
|
fixtureId: fixture.id,
|
|
224
287
|
event,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
cliPublicKey: uploadResult.cliPublicKey.toString("base64"),
|
|
288
|
+
uploadId: uploaded.uploadId,
|
|
289
|
+
cliPublicKey: uploaded.cliPublicKey,
|
|
228
290
|
secrets: opts.secrets,
|
|
229
291
|
...encrypted && {
|
|
230
292
|
encryptedSecrets: encrypted.encryptedSecrets,
|
|
231
293
|
encryptedSecretsKey: encrypted.cliPublicKey
|
|
232
294
|
},
|
|
233
295
|
workflowName: opts.workflowName,
|
|
234
|
-
inlineLockFile,
|
|
235
|
-
fullRepo:
|
|
296
|
+
inlineLockFile: overlay.inlineLockFile,
|
|
297
|
+
fullRepo: true
|
|
236
298
|
});
|
|
237
299
|
if (triggerResult.status === "rejected") {
|
|
238
300
|
if (!options.quiet) logger.info(pc.red(`Rejected: ${triggerResult.reason ?? "unknown reason"}`));
|
|
@@ -240,29 +302,23 @@ async function runSingleFixture(fixture, client, options, config, history) {
|
|
|
240
302
|
fixtureId: fixture.id,
|
|
241
303
|
runId: triggerResult.runId,
|
|
242
304
|
status: "rejected",
|
|
243
|
-
reason: triggerResult.reason
|
|
244
|
-
observeUrl: triggerResult.observeUrl
|
|
305
|
+
reason: triggerResult.reason
|
|
245
306
|
};
|
|
246
307
|
}
|
|
247
|
-
if (!options.quiet) {
|
|
248
|
-
logger.info(pc.green(`Run started: ${triggerResult.runId}`));
|
|
249
|
-
logger.info(pc.gray(`Observe: ${triggerResult.observeUrl}`));
|
|
250
|
-
}
|
|
251
|
-
const historyEndpoint = config.endpoint ?? config.platformEndpoint ?? "";
|
|
308
|
+
if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
|
|
252
309
|
await history.addEntry({
|
|
253
310
|
runId: triggerResult.runId,
|
|
254
311
|
fixtureId: fixture.id,
|
|
255
312
|
status: "running",
|
|
256
313
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
257
|
-
endpoint:
|
|
314
|
+
endpoint: config.platformEndpoint ?? ""
|
|
258
315
|
});
|
|
259
316
|
if (options.wait === false) return {
|
|
260
317
|
fixtureId: fixture.id,
|
|
261
318
|
runId: triggerResult.runId,
|
|
262
|
-
status: "accepted"
|
|
263
|
-
observeUrl: triggerResult.observeUrl
|
|
319
|
+
status: "accepted"
|
|
264
320
|
};
|
|
265
|
-
const result = await
|
|
321
|
+
const result = await pollRunToCompletion(ctx, triggerResult.runId, fixture.id, options);
|
|
266
322
|
await history.updateEntry(triggerResult.runId, {
|
|
267
323
|
status: result.status,
|
|
268
324
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -271,283 +327,168 @@ async function runSingleFixture(fixture, client, options, config, history) {
|
|
|
271
327
|
});
|
|
272
328
|
return result;
|
|
273
329
|
} catch (error) {
|
|
274
|
-
|
|
275
|
-
else if (error instanceof ConnectionError) logger.error(pc.red(`Connection failed: ${error.message}`));
|
|
276
|
-
return {
|
|
277
|
-
fixtureId: fixture.id,
|
|
278
|
-
runId: "",
|
|
279
|
-
status: "error",
|
|
280
|
-
reason: toErrorMessage(error)
|
|
281
|
-
};
|
|
330
|
+
return handleRunError(fixture.id, error, options);
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
|
-
/**
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
333
|
+
/** Map a run-path error to a CLI message + a result. */
|
|
334
|
+
function handleRunError(fixtureId, error, options) {
|
|
335
|
+
if (error instanceof AmbiguousClusterError) {
|
|
336
|
+
logger.error(pc.red(`Multiple orchestrators are connected. Pass --orchestrator <name>, one of: ${error.clusters.join(", ")}`));
|
|
337
|
+
logger.error(pc.gray("Or set a default with `kici orchestrators use <name>`."));
|
|
338
|
+
} else if (error instanceof NoClusterError) logger.error(pc.red("No orchestrator is connected for this organization."));
|
|
339
|
+
else if (error instanceof AuthenticationError) logger.error(pc.red("Authentication failed. Run `kici login` to re-authenticate."));
|
|
340
|
+
else if (error instanceof AccessDeniedError) logger.error(pc.red(`Access denied: ${error.message}`));
|
|
341
|
+
else if (error instanceof ConnectionError) logger.error(pc.red(`Connection failed: ${error.message}`));
|
|
342
|
+
else if (options.debug && error instanceof Error && error.stack) logger.error(pc.gray(error.stack));
|
|
292
343
|
return {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
payload: {
|
|
298
|
-
ref: `refs/heads/${targetBranch}`,
|
|
299
|
-
repository: { full_name: opts.repo ?? "owner/repo" },
|
|
300
|
-
...opts.sha && { after: opts.sha },
|
|
301
|
-
...opts.pr && { number: opts.pr }
|
|
302
|
-
}
|
|
344
|
+
fixtureId,
|
|
345
|
+
runId: "",
|
|
346
|
+
status: "error",
|
|
347
|
+
reason: toErrorMessage(error)
|
|
303
348
|
};
|
|
304
349
|
}
|
|
305
350
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
351
|
+
* Poll the Platform for run completion, streaming log lines as they arrive.
|
|
352
|
+
*
|
|
353
|
+
* Advances a monotonic line-offset cursor (spec §13a): each `runLogs(cursor)`
|
|
354
|
+
* returns the next chunk + `nextCursor`; the run is done only when the status
|
|
355
|
+
* is terminal and the log stream has drained.
|
|
308
356
|
*/
|
|
309
|
-
async function
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
let completionResult = null;
|
|
315
|
-
let fallbackToPolling = false;
|
|
316
|
-
const observer = new ObserverClient({
|
|
317
|
-
observeUrl,
|
|
318
|
-
token,
|
|
319
|
-
runId,
|
|
320
|
-
onLog: (msg) => {
|
|
321
|
-
for (const line of msg.lines) {
|
|
322
|
-
const output = formatter.formatLogLine(msg.jobName, msg.stepName, line);
|
|
323
|
-
if (output) process.stdout.write(output);
|
|
324
|
-
}
|
|
325
|
-
if (!lastLinesByJob.has(msg.jobName)) lastLinesByJob.set(msg.jobName, []);
|
|
326
|
-
const buffer = lastLinesByJob.get(msg.jobName);
|
|
327
|
-
buffer.push(...msg.lines);
|
|
328
|
-
while (buffer.length > MAX_ERROR_LINES) buffer.shift();
|
|
329
|
-
},
|
|
330
|
-
onStep: (msg) => {
|
|
331
|
-
const output = formatter.formatStepChange(msg.jobName, msg.stepName, msg.state, msg.durationMs);
|
|
332
|
-
if (output) process.stdout.write(output);
|
|
333
|
-
},
|
|
334
|
-
onStatus: (msg) => {
|
|
335
|
-
const output = formatter.formatStatusChange(msg.status, msg.jobName);
|
|
336
|
-
if (output) process.stdout.write(output);
|
|
337
|
-
},
|
|
338
|
-
onComplete: (msg) => {
|
|
339
|
-
completionResult = msg;
|
|
340
|
-
},
|
|
341
|
-
onError: (err) => {
|
|
342
|
-
if (options.debug) logger.error(pc.gray(`Observer error: ${err.message}`));
|
|
343
|
-
fallbackToPolling = true;
|
|
344
|
-
}
|
|
345
|
-
});
|
|
357
|
+
async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
let cursor = 0;
|
|
360
|
+
const tailLines = [];
|
|
361
|
+
const MAX_TAIL = 50;
|
|
346
362
|
let cancelled = false;
|
|
347
363
|
const cancelHandler = async () => {
|
|
348
364
|
if (cancelled) return;
|
|
349
365
|
cancelled = true;
|
|
350
366
|
if (!options.quiet) logger.info(pc.yellow("\nCancelling test run..."));
|
|
351
|
-
observer.disconnect();
|
|
352
367
|
try {
|
|
353
|
-
|
|
354
|
-
if (!options.quiet) logger.info(pc.gray(`Cancel status: ${cancelResult.status}`));
|
|
368
|
+
await ctx.client.cancel(ctx.orgId, runId, ctx.target);
|
|
355
369
|
} catch {}
|
|
356
370
|
};
|
|
357
371
|
process.on("SIGINT", cancelHandler);
|
|
358
372
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
formatter.startElapsedTimer();
|
|
367
|
-
let complete;
|
|
368
|
-
try {
|
|
369
|
-
complete = await observer.waitForCompletion();
|
|
370
|
-
} catch {
|
|
371
|
-
if (cancelled) return {
|
|
372
|
-
fixtureId,
|
|
373
|
-
runId,
|
|
374
|
-
status: "cancelled",
|
|
375
|
-
observeUrl
|
|
376
|
-
};
|
|
377
|
-
observer.disconnect();
|
|
378
|
-
process.removeListener("SIGINT", cancelHandler);
|
|
379
|
-
if (options.debug) logger.info(pc.gray("Observer disconnected, falling back to HTTP polling"));
|
|
380
|
-
return await pollForCompletionFallback(fixtureId, runId, observeUrl, client, options);
|
|
381
|
-
}
|
|
382
|
-
completionResult = complete;
|
|
383
|
-
formatter.stopElapsedTimer();
|
|
384
|
-
if (fallbackToPolling && !completionResult) {
|
|
385
|
-
observer.disconnect();
|
|
386
|
-
process.removeListener("SIGINT", cancelHandler);
|
|
387
|
-
if (options.debug) logger.info(pc.gray("Observer errored, falling back to HTTP polling"));
|
|
388
|
-
return await pollForCompletionFallback(fixtureId, runId, observeUrl, client, options);
|
|
389
|
-
}
|
|
390
|
-
const runResult = {
|
|
391
|
-
fixtureId,
|
|
392
|
-
runId,
|
|
393
|
-
status: complete.status,
|
|
394
|
-
totalDurationMs: complete.summary.totalDurationMs,
|
|
395
|
-
jobs: complete.summary.jobs
|
|
396
|
-
};
|
|
397
|
-
if (!options.quiet) process.stdout.write("\n" + formatSummary(runResult) + "\n");
|
|
398
|
-
if (complete.status === "failed") {
|
|
399
|
-
for (const job of complete.summary.jobs) if (job.status === "failed") {
|
|
400
|
-
const lines = lastLinesByJob.get(job.name);
|
|
401
|
-
if (lines && lines.length > 0 && !options.quiet) process.stdout.write("\n" + formatErrorHighlight(job.name, lines) + "\n");
|
|
373
|
+
let lastStatus = null;
|
|
374
|
+
while (!cancelled) {
|
|
375
|
+
const logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
|
|
376
|
+
for (const line of logs.lines) {
|
|
377
|
+
if (!options.quiet) process.stdout.write(line + "\n");
|
|
378
|
+
tailLines.push(line);
|
|
379
|
+
while (tailLines.length > MAX_TAIL) tailLines.shift();
|
|
402
380
|
}
|
|
381
|
+
cursor = logs.nextCursor;
|
|
382
|
+
lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
|
|
383
|
+
if ((lastStatus.done || TERMINAL_STATUSES.has(lastStatus.status)) && logs.done) return finishRun(fixtureId, runId, lastStatus, startTime, tailLines, options);
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
403
385
|
}
|
|
404
|
-
observer.disconnect();
|
|
405
386
|
return {
|
|
406
387
|
fixtureId,
|
|
407
388
|
runId,
|
|
408
|
-
status:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
jobs: complete.summary.jobs
|
|
389
|
+
status: "cancelled",
|
|
390
|
+
durationMs: Date.now() - startTime,
|
|
391
|
+
jobs: jobsFromStatus(lastStatus)
|
|
412
392
|
};
|
|
413
393
|
} finally {
|
|
414
|
-
formatter.stopElapsedTimer();
|
|
415
394
|
process.removeListener("SIGINT", cancelHandler);
|
|
416
395
|
}
|
|
417
396
|
}
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
let cancelled = false;
|
|
426
|
-
const cancelHandler = async () => {
|
|
427
|
-
if (cancelled) return;
|
|
428
|
-
cancelled = true;
|
|
429
|
-
if (!options.quiet) logger.info(pc.yellow("\nCancelling test run..."));
|
|
430
|
-
try {
|
|
431
|
-
const cancelResult = await client.cancelTest(runId);
|
|
432
|
-
if (!options.quiet) logger.info(pc.gray(`Cancel status: ${cancelResult.status}`));
|
|
433
|
-
} catch {}
|
|
434
|
-
};
|
|
435
|
-
process.on("SIGINT", cancelHandler);
|
|
436
|
-
try {
|
|
437
|
-
while (!cancelled) {
|
|
438
|
-
if (Date.now() - startTime > MAX_POLL_DURATION_MS) return {
|
|
439
|
-
fixtureId,
|
|
440
|
-
runId,
|
|
441
|
-
status: "error",
|
|
442
|
-
reason: "Polling timeout exceeded (30 minutes)",
|
|
443
|
-
observeUrl
|
|
444
|
-
};
|
|
445
|
-
try {
|
|
446
|
-
const status = await client.getRunStatus(runId);
|
|
447
|
-
if (status.status === "success" || status.status === "failed" || status.status === "cancelled") {
|
|
448
|
-
const durationMs = Date.now() - startTime;
|
|
449
|
-
if (!options.quiet) {
|
|
450
|
-
const statusColor = status.status === "success" ? pc.green : pc.red;
|
|
451
|
-
logger.info(statusColor(`\nRun ${status.status} (${(durationMs / 1e3).toFixed(1)}s)`));
|
|
452
|
-
if (status.summary?.jobs) for (const job of status.summary.jobs) {
|
|
453
|
-
const jobColor = job.status === "success" ? pc.green : pc.red;
|
|
454
|
-
const duration = job.durationMs ? ` (${(job.durationMs / 1e3).toFixed(1)}s)` : "";
|
|
455
|
-
logger.info(` ${jobColor(job.status.padEnd(10))} ${job.name}${duration}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
fixtureId,
|
|
460
|
-
runId,
|
|
461
|
-
status: status.status,
|
|
462
|
-
observeUrl,
|
|
463
|
-
durationMs: Date.now() - startTime,
|
|
464
|
-
jobs: status.summary?.jobs
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
} catch {}
|
|
468
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
397
|
+
/** Build the final result + render the summary table for a completed run. */
|
|
398
|
+
function finishRun(fixtureId, runId, status, startTime, tailLines, options) {
|
|
399
|
+
const durationMs = Date.now() - startTime;
|
|
400
|
+
const jobs = jobsFromStatus(status);
|
|
401
|
+
const finalStatus = status.status;
|
|
402
|
+
if (!options.quiet) {
|
|
403
|
+
const runResult = {
|
|
471
404
|
fixtureId,
|
|
472
405
|
runId,
|
|
473
|
-
status: "cancelled",
|
|
474
|
-
|
|
475
|
-
|
|
406
|
+
status: finalStatus === "failed" ? "failed" : finalStatus === "cancelled" ? "cancelled" : "success",
|
|
407
|
+
totalDurationMs: durationMs,
|
|
408
|
+
jobs
|
|
476
409
|
};
|
|
477
|
-
|
|
478
|
-
|
|
410
|
+
process.stdout.write("\n" + formatSummary(runResult) + "\n");
|
|
411
|
+
if (finalStatus === "failed" && tailLines.length > 0) {
|
|
412
|
+
const failedJob = status.jobs.find((j) => j.status === "failed");
|
|
413
|
+
process.stdout.write("\n" + formatErrorHighlight(failedJob?.jobName ?? "run", tailLines) + "\n");
|
|
414
|
+
}
|
|
479
415
|
}
|
|
416
|
+
return {
|
|
417
|
+
fixtureId,
|
|
418
|
+
runId,
|
|
419
|
+
status: finalStatus,
|
|
420
|
+
durationMs,
|
|
421
|
+
jobs
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/** Map a Platform run-status snapshot to the CLI's job summary shape. */
|
|
425
|
+
function jobsFromStatus(status) {
|
|
426
|
+
if (!status) return [];
|
|
427
|
+
return status.jobs.map((j) => ({
|
|
428
|
+
name: j.jobName,
|
|
429
|
+
status: j.status
|
|
430
|
+
}));
|
|
480
431
|
}
|
|
481
432
|
/**
|
|
482
|
-
*
|
|
433
|
+
* Build a SimulatedEvent-compatible object from fixture options.
|
|
434
|
+
*/
|
|
435
|
+
function buildEventFromFixture(opts) {
|
|
436
|
+
const eventObj = opts.event;
|
|
437
|
+
const type = String(eventObj._type ?? "push");
|
|
438
|
+
const action = eventObj.action ? String(eventObj.action) : void 0;
|
|
439
|
+
const targetBranch = opts.branch ?? "main";
|
|
440
|
+
return {
|
|
441
|
+
type,
|
|
442
|
+
action,
|
|
443
|
+
targetBranch,
|
|
444
|
+
sourceBranch: type === "pr" ? opts.branch ?? "feature/test" : void 0,
|
|
445
|
+
payload: {
|
|
446
|
+
ref: `refs/heads/${targetBranch}`,
|
|
447
|
+
repository: { full_name: opts.repo ?? "owner/repo" },
|
|
448
|
+
...opts.sha && { after: opts.sha },
|
|
449
|
+
...opts.pr && { number: opts.pr }
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Run a specific workflow directly (bypass trigger matching), through the
|
|
455
|
+
* Platform.
|
|
483
456
|
*/
|
|
484
457
|
async function runDirectWorkflow(workflowName, options) {
|
|
485
|
-
const
|
|
486
|
-
if (!
|
|
487
|
-
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
const endpoint = config.endpoint ?? config.platformEndpoint;
|
|
491
|
-
if (!endpoint) {
|
|
492
|
-
logger.error(pc.red("No orchestrator endpoint configured. Run `kici login --endpoint <url>` to set one."));
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
const client = new OrchestratorClient({
|
|
496
|
-
endpoint,
|
|
497
|
-
token: config.token
|
|
498
|
-
});
|
|
458
|
+
const ctx = resolvePlatformContext(await loadGlobalConfig(), options);
|
|
459
|
+
if (!ctx) return false;
|
|
460
|
+
if (options.json) options.quiet = true;
|
|
499
461
|
if (!options.quiet) logger.info(pc.gray(`Running workflow "${workflowName}" directly (bypassing triggers)`));
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
type: "manual",
|
|
530
|
-
targetBranch: "main",
|
|
531
|
-
payload
|
|
532
|
-
},
|
|
533
|
-
routingKey,
|
|
534
|
-
uploadId: upload.uploadId,
|
|
535
|
-
cliPublicKey: directUploadResult.cliPublicKey.toString("base64"),
|
|
536
|
-
...encrypted && {
|
|
537
|
-
encryptedSecrets: encrypted.encryptedSecrets,
|
|
538
|
-
encryptedSecretsKey: encrypted.cliPublicKey
|
|
539
|
-
},
|
|
540
|
-
workflowName,
|
|
541
|
-
inlineLockFile,
|
|
542
|
-
fullRepo: !hasRemote || void 0
|
|
543
|
-
});
|
|
544
|
-
if (!options.quiet) {
|
|
545
|
-
logger.info(pc.green(`Run started: ${result.runId}`));
|
|
546
|
-
logger.info(pc.gray(`Observe: ${result.observeUrl}`));
|
|
462
|
+
await new RunHistory().load();
|
|
463
|
+
const fixtureId = `direct:${workflowName}`;
|
|
464
|
+
try {
|
|
465
|
+
const overlay = await prepareOverlay(options);
|
|
466
|
+
const uploaded = await initAndUpload(ctx, overlay, options);
|
|
467
|
+
const payload = { repository: { full_name: `local/${path.basename(overlay.repoRoot)}` } };
|
|
468
|
+
const encrypted = await buildEncryptedSecrets(overlay.kiciDir, options.envFlags, options.context, uploaded.publicKey);
|
|
469
|
+
const triggerResult = await ctx.client.trigger(ctx.orgId, ctx.target, {
|
|
470
|
+
fixtureId,
|
|
471
|
+
event: {
|
|
472
|
+
type: "manual",
|
|
473
|
+
targetBranch: "main",
|
|
474
|
+
payload
|
|
475
|
+
},
|
|
476
|
+
uploadId: uploaded.uploadId,
|
|
477
|
+
cliPublicKey: uploaded.cliPublicKey,
|
|
478
|
+
...encrypted && {
|
|
479
|
+
encryptedSecrets: encrypted.encryptedSecrets,
|
|
480
|
+
encryptedSecretsKey: encrypted.cliPublicKey
|
|
481
|
+
},
|
|
482
|
+
workflowName,
|
|
483
|
+
inlineLockFile: overlay.inlineLockFile,
|
|
484
|
+
fullRepo: true
|
|
485
|
+
});
|
|
486
|
+
if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
|
|
487
|
+
if (options.wait === false) return triggerResult.status === "accepted";
|
|
488
|
+
return (await pollRunToCompletion(ctx, triggerResult.runId, fixtureId, options)).status === "success";
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return handleRunError(fixtureId, error, options).status === "success";
|
|
547
491
|
}
|
|
548
|
-
if (options.wait === false) return result.status === "accepted";
|
|
549
|
-
const observeResult = await observeCompletion(`direct:${workflowName}`, result.runId, result.observeUrl, config.token, client, options);
|
|
550
|
-
return observeResult.status === "success" || observeResult.status === "accepted";
|
|
551
492
|
}
|
|
552
493
|
/**
|
|
553
494
|
* Display remote run results as a summary table.
|
|
@@ -568,6 +509,6 @@ function displayRemoteResults(results) {
|
|
|
568
509
|
logger.info("");
|
|
569
510
|
}
|
|
570
511
|
//#endregion
|
|
571
|
-
export {
|
|
512
|
+
export { runLocalCommand, runRemoteCommand };
|
|
572
513
|
|
|
573
514
|
//# sourceMappingURL=run.js.map
|