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 path = require("path");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const fs = require("fs");
|
||||||
const { WebSocket } = require("ws");
|
const { WebSocket } = require("ws");
|
||||||
|
|
||||||
const TEST_DB = path.join(os.tmpdir(), `dashboard-lanes-api-${Date.now()}-${process.pid}.db`);
|
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}`);
|
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");
|
} = require("../lib/lane-runtime");
|
||||||
const { reapOrphanMcp, capOversizedLogs } = require("../lib/lane-gc");
|
const { reapOrphanMcp, capOversizedLogs } = require("../lib/lane-gc");
|
||||||
const { detectNode, scaffoldProfile } = require("../lib/lane-detect");
|
const { detectNode, scaffoldProfile } = require("../lib/lane-detect");
|
||||||
|
const { ciStatus } = require("../lib/ci-status");
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const MAX_WORKTREE_DIRECTORY_ATTEMPTS = 50;
|
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
|
* Create a dashboard-managed worktree lane. Git work happens after the 202
|
||||||
* response because provisioning a large repository can take seconds.
|
* response because provisioning a large repository can take seconds.
|
||||||
|
|||||||
Reference in New Issue
Block a user