feat(cli): add ccam lanes ci status
This commit is contained in:
+40
@@ -2263,6 +2263,38 @@ async function cmdLanesIntegration(args) {
|
|||||||
process.exitCode = enabled ? 0 : 1;
|
process.exitCode = enabled ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `ccam lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
|
||||||
|
* Actions status snapshot for a ref, via GET /:id/ci/status. Never polls;
|
||||||
|
* the caller (ship-feature-lane Stage 13) owns the wait loop. Exit code
|
||||||
|
* mirrors the derived state: 0 success, 1 failure or any error, 3 pending. */
|
||||||
|
async function cmdLanesCi(args) {
|
||||||
|
if (args[0] !== "status") {
|
||||||
|
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ref = args[1];
|
||||||
|
if (!ref) {
|
||||||
|
console.error("usage: ccam lanes ci status <branch-or-sha> [<lane-id>]");
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resolved = await resolveLaneArg(args.slice(2));
|
||||||
|
if (!resolved) return;
|
||||||
|
const result = await get(
|
||||||
|
`/api/lanes/${resolved.laneId}/ci/status?ref=${encodeURIComponent(ref)}`,
|
||||||
|
undefined,
|
||||||
|
{ allowError: true }
|
||||||
|
);
|
||||||
|
if (result.status) {
|
||||||
|
console.error(`✖ ci status → ${result.data?.error?.message || result.status}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
process.exitCode = result.state === "success" ? 0 : result.state === "pending" ? 3 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `ccam lanes pipeline [<template-id>] [<id>]` — show or switch which pipeline
|
* `ccam lanes pipeline [<template-id>] [<id>]` — show or switch which pipeline
|
||||||
* template a lane renders against. `ccam lanes add --pipeline` could only set
|
* template a lane renders against. `ccam lanes add --pipeline` could only set
|
||||||
@@ -2552,6 +2584,11 @@ const COMMAND_GROUPS = [
|
|||||||
"<name> [<id>]",
|
"<name> [<id>]",
|
||||||
"Check whether a named integration (tracker, dev_qc, ci_wait) is on for this lane — exit 0/1",
|
"Check whether a named integration (tracker, dev_qc, ci_wait) is on for this lane — exit 0/1",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"lanes ci status",
|
||||||
|
"<branch-or-sha> [<id>]",
|
||||||
|
"One-shot GitHub Actions status for a ref — exit 0 success / 1 failure / 3 pending",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"lanes gc",
|
"lanes gc",
|
||||||
"[--dry-run]",
|
"[--dry-run]",
|
||||||
@@ -3389,6 +3426,9 @@ async function runCommand(argv) {
|
|||||||
if (rest[0] === "integration") {
|
if (rest[0] === "integration") {
|
||||||
return cmdLanesIntegration(rest.slice(1));
|
return cmdLanesIntegration(rest.slice(1));
|
||||||
}
|
}
|
||||||
|
if (rest[0] === "ci") {
|
||||||
|
return cmdLanesCi(rest.slice(1));
|
||||||
|
}
|
||||||
if (rest[0] === "pipeline") {
|
if (rest[0] === "pipeline") {
|
||||||
return cmdLanesPipeline(rest.slice(1));
|
return cmdLanesPipeline(rest.slice(1));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,11 +126,11 @@ function get(urlPath) {
|
|||||||
// spawnSync would stall the event loop and the CLI child's request to
|
// spawnSync would stall the event loop and the CLI child's request to
|
||||||
// 127.0.0.1 would never be served — a deadlock that looks like a network
|
// 127.0.0.1 would never be served — a deadlock that looks like a network
|
||||||
// sandbox blocking loopback.
|
// sandbox blocking loopback.
|
||||||
function cli(args, cwd) {
|
function cli(args, cwd, extraEnv = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(process.execPath, [CLI, ...args], {
|
const child = spawn(process.execPath, [CLI, ...args], {
|
||||||
cwd,
|
cwd,
|
||||||
env: { ...process.env, CLAUDE_DASHBOARD_PORT: String(server.address().port) },
|
env: { ...process.env, CLAUDE_DASHBOARD_PORT: String(server.address().port), ...extraEnv },
|
||||||
});
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
@@ -542,3 +542,77 @@ describe("ccam lanes — inferred stage detection", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ccam lanes ci status", () => {
|
||||||
|
let restorePath;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-cli-bin-"));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(bin, "gh"),
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
if [ "$1" = "run" ] && [ "$2" = "list" ]; then
|
||||||
|
if [ "$CI_STATUS_FIXTURE" = "failure" ]; then
|
||||||
|
echo '[{"databaseId":1,"status":"completed","conclusion":"failure"}]'
|
||||||
|
elif [ "$CI_STATUS_FIXTURE" = "pending" ]; then
|
||||||
|
echo '[]'
|
||||||
|
else
|
||||||
|
echo '[{"databaseId":1,"status":"completed","conclusion":"success"}]'
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$1" = "run" ] && [ "$2" = "view" ]; then
|
||||||
|
if [ "$CI_STATUS_FIXTURE" = "failure" ]; then
|
||||||
|
echo '{"jobs":[{"name":"tests","status":"completed","conclusion":"failure","url":"https://x/1"}]}'
|
||||||
|
else
|
||||||
|
echo '{"jobs":[{"name":"tests","status":"completed","conclusion":"success","url":"https://x/1"}]}'
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
`
|
||||||
|
);
|
||||||
|
fs.chmodSync(path.join(bin, "gh"), 0o755);
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
|
||||||
|
restorePath = () => {
|
||||||
|
process.env.PATH = originalPath;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => restorePath && restorePath());
|
||||||
|
|
||||||
|
it("exits 0 and prints success state", async () => {
|
||||||
|
const r = await cli(["lanes", "ci", "status", "development"], LANE_DIR);
|
||||||
|
assert.equal(r.status, 0);
|
||||||
|
assert.match(r.stdout, /"state": "success"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exits 1 on a failed run", async () => {
|
||||||
|
const orig = process.env.CI_STATUS_FIXTURE;
|
||||||
|
try {
|
||||||
|
process.env.CI_STATUS_FIXTURE = "failure";
|
||||||
|
const r = await cli(["lanes", "ci", "status", "development"], LANE_DIR, {
|
||||||
|
CI_STATUS_FIXTURE: "failure",
|
||||||
|
});
|
||||||
|
assert.equal(r.status, 1);
|
||||||
|
assert.match(r.stdout, /"state": "failure"/);
|
||||||
|
} finally {
|
||||||
|
process.env.CI_STATUS_FIXTURE = orig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exits 3 when no run exists yet", async () => {
|
||||||
|
const orig = process.env.CI_STATUS_FIXTURE;
|
||||||
|
try {
|
||||||
|
process.env.CI_STATUS_FIXTURE = "pending";
|
||||||
|
const r = await cli(["lanes", "ci", "status", "development"], LANE_DIR, {
|
||||||
|
CI_STATUS_FIXTURE: "pending",
|
||||||
|
});
|
||||||
|
assert.equal(r.status, 3);
|
||||||
|
assert.match(r.stdout, /"state": "pending"/);
|
||||||
|
} finally {
|
||||||
|
process.env.CI_STATUS_FIXTURE = orig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+5
-1
@@ -23,7 +23,11 @@ const execFileAsync = promisify(execFile);
|
|||||||
*/
|
*/
|
||||||
async function gh(cwd, args) {
|
async function gh(cwd, args) {
|
||||||
try {
|
try {
|
||||||
const result = await execFileAsync("gh", args, { cwd, maxBuffer: 8 * 1024 * 1024 });
|
const result = await execFileAsync("gh", args, {
|
||||||
|
cwd,
|
||||||
|
env: process.env,
|
||||||
|
maxBuffer: 8 * 1024 * 1024,
|
||||||
|
});
|
||||||
return { stdout: result.stdout, stderr: result.stderr };
|
return { stdout: result.stdout, stderr: result.stderr };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = new Error(`gh ${args[0]} failed`);
|
const error = new Error(`gh ${args[0]} failed`);
|
||||||
|
|||||||
Reference in New Issue
Block a user