From db9b1fcafa30defbe0ee35b2eeb095f4506db44e Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 25 Aug 2026 09:26:33 +0700 Subject: [PATCH] feat(api): add GET /:id/ci/status --- server/__tests__/lanes-api.test.js | 58 ++++++++++++++++++++++++++++++ server/routes/lanes.js | 22 ++++++++++++ 2 files changed, 80 insertions(+) diff --git a/server/__tests__/lanes-api.test.js b/server/__tests__/lanes-api.test.js index db346405..b3cc58d8 100644 --- a/server/__tests__/lanes-api.test.js +++ b/server/__tests__/lanes-api.test.js @@ -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"); + }); +}); diff --git a/server/routes/lanes.js b/server/routes/lanes.js index 0ef1a888..7f32ba17 100644 --- a/server/routes/lanes.js +++ b/server/routes/lanes.js @@ -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.