feat(api): add GET /:id/ci/status
This commit is contained in:
@@ -9,6 +9,7 @@ const assert = require("node:assert/strict");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const { WebSocket } = require("ws");
|
||||
|
||||
const TEST_DB = path.join(os.tmpdir(), `dashboard-lanes-api-${Date.now()}-${process.pid}.db`);
|
||||
@@ -928,3 +929,60 @@ describe("detection attributes work to the lane the work touched", () => {
|
||||
await request("DELETE", `/api/lanes/${session.body.lane.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /:id/ci/status", () => {
|
||||
let laneId;
|
||||
let laneCwd;
|
||||
let restorePath;
|
||||
|
||||
before(async () => {
|
||||
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-route-"));
|
||||
const r = await request("POST", "/api/lanes", { title: "CI status lane", cwd: laneCwd });
|
||||
laneId = r.body.lane.id;
|
||||
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
if [ "$1" = "run" ] && [ "$2" = "list" ]; then
|
||||
echo '[{"databaseId":1,"status":"completed","conclusion":"success"}]'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "run" ] && [ "$2" = "view" ]; then
|
||||
echo '{"jobs":[{"name":"tests","status":"completed","conclusion":"success","url":"https://x/1"}]}'
|
||||
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(() => {
|
||||
if (restorePath) restorePath();
|
||||
fs.rmSync(laneCwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("404s for an unknown lane", async () => {
|
||||
const r = await request("GET", "/api/lanes/999999/ci/status?ref=development");
|
||||
assert.equal(r.status, 404);
|
||||
});
|
||||
|
||||
it("400s when ref is missing", async () => {
|
||||
const r = await request("GET", `/api/lanes/${laneId}/ci/status`);
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(r.body.error.code, "EBADREF");
|
||||
});
|
||||
|
||||
it("returns the derived state for a real ref", async () => {
|
||||
const r = await request("GET", `/api/lanes/${laneId}/ci/status?ref=development`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(r.body.state, "success");
|
||||
assert.equal(r.body.checks[0].name, "tests");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ const {
|
||||
} = require("../lib/lane-runtime");
|
||||
const { reapOrphanMcp, capOversizedLogs } = require("../lib/lane-gc");
|
||||
const { detectNode, scaffoldProfile } = require("../lib/lane-detect");
|
||||
const { ciStatus } = require("../lib/ci-status");
|
||||
|
||||
const router = Router();
|
||||
const MAX_WORKTREE_DIRECTORY_ATTEMPTS = 50;
|
||||
@@ -520,6 +521,27 @@ router.get("/:id/git", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A single GitHub Actions status snapshot for one ref (branch or commit sha)
|
||||
* in the lane's repo — NOT a poll loop. ship-feature-lane's Stage 13 owns
|
||||
* the wait/retry decision (docs/LANES.md "Orchestration: what CCAM does NOT
|
||||
* do"); this route only reports what `gh` currently knows. Read-only, no
|
||||
* same-origin guard, same reasoning as GET /:id/git.
|
||||
*/
|
||||
router.get("/:id/ci/status", async (req, res) => {
|
||||
const lane = laneOr404(req, res);
|
||||
if (!lane) return;
|
||||
const ref = typeof req.query.ref === "string" ? req.query.ref.trim() : "";
|
||||
if (!ref) {
|
||||
return res.status(400).json({ error: { code: "EBADREF", message: "ref is required" } });
|
||||
}
|
||||
try {
|
||||
res.json(await ciStatus(lane.cwd, ref));
|
||||
} catch (err) {
|
||||
sendRuntimeError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a dashboard-managed worktree lane. Git work happens after the 202
|
||||
* response because provisioning a large repository can take seconds.
|
||||
|
||||
Reference in New Issue
Block a user