feat(lanes): add POST /:id/sync-base route (E2)
This commit is contained in:
+53
-1
@@ -32,6 +32,7 @@ const {
|
||||
slugify,
|
||||
} = require("../lib/worktree");
|
||||
const { withLaneLock } = require("../lib/lane-lock");
|
||||
const { checkSync, mergeSync, continueSync } = require("../lib/lane-sync");
|
||||
const { HOOKS, runHook, resolveProfile } = require("../lib/lane-profile");
|
||||
const { slotDirs } = require("../lib/lane-slots");
|
||||
const {
|
||||
@@ -563,7 +564,15 @@ function sendLifecycleError(res, err) {
|
||||
|
||||
/** Map a runtime error onto its status code. */
|
||||
function sendRuntimeError(res, err) {
|
||||
const badRequest = ["ENOPROFILE", "ENOHOOK", "EBADLANEDIR", "EBADSVC"];
|
||||
const badRequest = [
|
||||
"ENOPROFILE",
|
||||
"ENOHOOK",
|
||||
"EBADLANEDIR",
|
||||
"EBADSVC",
|
||||
"EBADBRANCH",
|
||||
"EUNRESOLVED",
|
||||
"EMERGEUNCOMMITTED",
|
||||
];
|
||||
if (badRequest.includes(err.code)) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
@@ -759,6 +768,49 @@ router.post("/:id/hook/:name", sameOriginGuard, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The ONE sanctioned merge in the ship-feature-lane pipeline: origin/development
|
||||
* INTO a feature branch, gated by a migration-number collision preflight.
|
||||
* Synchronous — a fetch + collision-check + merge is seconds of git work, not
|
||||
* the minutes a build/test hook can take, so this follows GET /:id/git's
|
||||
* pattern rather than the hook route's 202-and-broadcast.
|
||||
*
|
||||
* Returns 200 with {code: 0|4|5, ...} for every DOCUMENTED outcome — a
|
||||
* migration collision or a left-in-place conflict is an expected result, not
|
||||
* an HTTP error. A malformed request, a missing profile, or an out-of-order
|
||||
* --continue is the only case that answers with an `error` body.
|
||||
*
|
||||
* Never writes stage/status/notes — same boundary the hook and runtime
|
||||
* routes already keep; the caller decides what a collision or conflict means
|
||||
* for the lane's declared stage.
|
||||
*/
|
||||
router.post("/:id/sync-base", sameOriginGuard, async (req, res) => {
|
||||
const lane = laneOr404(req, res);
|
||||
if (!lane) return;
|
||||
let profile;
|
||||
try {
|
||||
profile = requireProfile(lane);
|
||||
} catch (err) {
|
||||
return sendRuntimeError(res, err);
|
||||
}
|
||||
|
||||
const mode = ["check", "merge", "continue"].includes(req.body?.mode) ? req.body.mode : "merge";
|
||||
const branch =
|
||||
typeof req.body?.branch === "string" && req.body.branch ? req.body.branch : undefined;
|
||||
|
||||
try {
|
||||
const result = await withLaneLock(lane.id, () => {
|
||||
const current = lanesLib.getLane(lane.id);
|
||||
if (mode === "check") return checkSync(current, profile, branch);
|
||||
if (mode === "continue") return continueSync(current, profile, branch);
|
||||
return mergeSync(current, profile, branch);
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
sendRuntimeError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Lane control. Deliberately thin: every action maps onto one existing
|
||||
* run-spawner call. There is no queue, no chaining, no gate evaluation — the
|
||||
|
||||
Reference in New Issue
Block a user