@code-partner/codepipe-hub 0.14.1-dev.419.g21c1b62a → 0.14.1-dev.422.gf90f06c4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{ModelCanvas-WXJ1hqXD.js → ModelCanvas-CKATjkFe.js} +1 -1
- package/dashboard/dist/assets/{index-K3ia3VFf.js → index-D5-hoCRk.js} +72 -72
- package/dashboard/dist/index.html +1 -1
- package/hub/dist/embedded.js +655 -212
- package/hub/dist/index.js +954 -844
- package/package.json +1 -1
package/hub/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ var PLATFORM_VERSION;
|
|
|
13
13
|
var init_version_generated = __esm({
|
|
14
14
|
"../packages/shared/dist/version.generated.js"() {
|
|
15
15
|
"use strict";
|
|
16
|
-
PLATFORM_VERSION = "0.14.1-dev.
|
|
16
|
+
PLATFORM_VERSION = "0.14.1-dev.422.gf90f06c4";
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -10580,6 +10580,15 @@ function humanSubjectOf(actor) {
|
|
|
10580
10580
|
return actor.principal.delegatedBy ?? null;
|
|
10581
10581
|
return null;
|
|
10582
10582
|
}
|
|
10583
|
+
function accountOf(actor) {
|
|
10584
|
+
const subjectId = humanSubjectOf(actor);
|
|
10585
|
+
if (subjectId === null)
|
|
10586
|
+
return null;
|
|
10587
|
+
return {
|
|
10588
|
+
subjectId,
|
|
10589
|
+
email: actor.principal.kind === "human" && actor.displayEmail !== void 0 ? actor.displayEmail : null
|
|
10590
|
+
};
|
|
10591
|
+
}
|
|
10583
10592
|
function serviceOf(actor) {
|
|
10584
10593
|
return actor.principal.kind === "service" ? actor.principal.service : null;
|
|
10585
10594
|
}
|
|
@@ -11488,16 +11497,40 @@ function createReadOperations(port, authorizer) {
|
|
|
11488
11497
|
return { projectId, items: rows, revision: valueRevision(rows) };
|
|
11489
11498
|
}
|
|
11490
11499
|
function projectVisibilityOf(actor) {
|
|
11491
|
-
const
|
|
11492
|
-
if (
|
|
11500
|
+
const account = accountOf(actor);
|
|
11501
|
+
if (account === null)
|
|
11493
11502
|
return null;
|
|
11494
11503
|
return {
|
|
11495
|
-
subjectId,
|
|
11496
|
-
...
|
|
11504
|
+
subjectId: account.subjectId,
|
|
11505
|
+
...account.email === null ? {} : { email: account.email },
|
|
11497
11506
|
...actor.projectScope === void 0 ? {} : { projectScope: actor.projectScope },
|
|
11498
11507
|
includeUnowned: actor.authMethod !== "cli_token"
|
|
11499
11508
|
};
|
|
11500
11509
|
}
|
|
11510
|
+
function farmsView(rows) {
|
|
11511
|
+
return {
|
|
11512
|
+
items: rows.map((r) => ({
|
|
11513
|
+
id: r.farmId,
|
|
11514
|
+
name: r.name,
|
|
11515
|
+
publicKey: r.publicKey,
|
|
11516
|
+
keyFingerprint: r.keyFingerprint,
|
|
11517
|
+
online: r.online,
|
|
11518
|
+
lastSeenAt: toOptionalTimestamp(r.lastSeenAt)
|
|
11519
|
+
}))
|
|
11520
|
+
};
|
|
11521
|
+
}
|
|
11522
|
+
function projectDeliverablesView(projectId, rows) {
|
|
11523
|
+
return {
|
|
11524
|
+
projectId,
|
|
11525
|
+
items: rows.map((r) => ({
|
|
11526
|
+
taskKey: r.taskKey,
|
|
11527
|
+
title: r.title,
|
|
11528
|
+
phase: r.phase,
|
|
11529
|
+
updatedAt: toTimestamp(r.updatedAt),
|
|
11530
|
+
repos: r.repos.map((x) => ({ repoName: x.repoName, branchName: x.branchName, baseRef: x.baseRef }))
|
|
11531
|
+
}))
|
|
11532
|
+
};
|
|
11533
|
+
}
|
|
11501
11534
|
async function visibleProjects(actor) {
|
|
11502
11535
|
const visibility = projectVisibilityOf(actor);
|
|
11503
11536
|
return visibility === null ? [] : await port.visibleProjectIds(visibility);
|
|
@@ -11966,6 +11999,15 @@ function createReadOperations(port, authorizer) {
|
|
|
11966
11999
|
const rows = await port.listRepos(query.projectId);
|
|
11967
12000
|
return reposView(query.projectId, rows);
|
|
11968
12001
|
},
|
|
12002
|
+
async listProjectDeliverables(actor, query) {
|
|
12003
|
+
await require2(actor, "delivery.read", { type: "deliverable", projectId: query.projectId });
|
|
12004
|
+
return projectDeliverablesView(query.projectId, await port.listProjectDeliverables(query.projectId));
|
|
12005
|
+
},
|
|
12006
|
+
async listFarms(actor) {
|
|
12007
|
+
await require2(actor, "account.read", { type: "account" });
|
|
12008
|
+
const account = accountOf(actor);
|
|
12009
|
+
return farmsView(account === null ? [] : await port.listFarms(account));
|
|
12010
|
+
},
|
|
11969
12011
|
async listConfigProposals(actor, query) {
|
|
11970
12012
|
await require2(actor, "project.read", { type: "project", id: query.projectId, projectId: query.projectId });
|
|
11971
12013
|
const limit = normalizeLimit(query.limit);
|
|
@@ -12195,8 +12237,16 @@ function createCommandOperations(deps) {
|
|
|
12195
12237
|
});
|
|
12196
12238
|
case "unprocessable":
|
|
12197
12239
|
throw errors.unprocessable({ details: { reason: outcome.reason } });
|
|
12240
|
+
case "not_found":
|
|
12241
|
+
throw errors.notFound();
|
|
12198
12242
|
}
|
|
12199
12243
|
}
|
|
12244
|
+
function accountActorOf(actor) {
|
|
12245
|
+
const account = accountOf(actor);
|
|
12246
|
+
if (account === null)
|
|
12247
|
+
throw errors.forbidden({ details: { reason: "human_only" } });
|
|
12248
|
+
return account;
|
|
12249
|
+
}
|
|
12200
12250
|
async function audited(actor, entry, run) {
|
|
12201
12251
|
const policy = { decision: "not_evaluated" };
|
|
12202
12252
|
const common = {
|
|
@@ -12685,6 +12735,107 @@ function createCommandOperations(deps) {
|
|
|
12685
12735
|
return { value: outcome.result, replayed: outcome.replayed };
|
|
12686
12736
|
});
|
|
12687
12737
|
},
|
|
12738
|
+
async createProject(actor, command) {
|
|
12739
|
+
const { project } = command;
|
|
12740
|
+
const projectId = project.projectId;
|
|
12741
|
+
if (!PROJECT_ID_RE.test(projectId)) {
|
|
12742
|
+
throw errors.invalidInput({
|
|
12743
|
+
message: "A project id is lowercase letters, digits and dashes, starts with a letter, 2\u201331 characters.",
|
|
12744
|
+
details: { field: "projectId" }
|
|
12745
|
+
});
|
|
12746
|
+
}
|
|
12747
|
+
for (const repo of project.repos) {
|
|
12748
|
+
if (repo.defaultBranch !== void 0 && !isValidBranchName(repo.defaultBranch)) {
|
|
12749
|
+
throw errors.invalidInput({ message: "That is not a valid git branch name.", details: { field: "repos.defaultBranch", repo: repo.name } });
|
|
12750
|
+
}
|
|
12751
|
+
}
|
|
12752
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12753
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "project.create", projectId, key };
|
|
12754
|
+
return await audited(actor, { operationId: "project.create", aggregateType: "project", aggregateId: projectId, projectId, data: { name: project.name, tracker: project.trackerProvider } }, async (policy) => {
|
|
12755
|
+
await require2(actor, "account.manage", { type: "account" }, policy);
|
|
12756
|
+
const run = await runIdempotent(idempotency, scope, { project }, async () => unwrap(await write2.createProject({ project, owner: accountActorOf(actor) })), now());
|
|
12757
|
+
return { value: run.result, replayed: run.replayed };
|
|
12758
|
+
});
|
|
12759
|
+
},
|
|
12760
|
+
async provisionOnFarm(actor, command) {
|
|
12761
|
+
const { farmId, projectId } = command;
|
|
12762
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12763
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "farm.provision", projectId, key };
|
|
12764
|
+
return await audited(actor, { operationId: "farm.provision", aggregateType: "project", aggregateId: projectId, projectId, data: { farmId } }, async (policy) => {
|
|
12765
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12766
|
+
const run = await runIdempotent(idempotency, scope, { farmId, projectId }, async () => {
|
|
12767
|
+
const row = unwrap(await write2.provisionOnFarm({ farmId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
|
|
12768
|
+
return { projectId, farmId, ok: row.ok, daemonStatus: row.daemonStatus, error: row.error };
|
|
12769
|
+
}, now());
|
|
12770
|
+
return { value: run.result, replayed: run.replayed };
|
|
12771
|
+
});
|
|
12772
|
+
},
|
|
12773
|
+
async provisionGhRunners(actor, command) {
|
|
12774
|
+
const { runnerId, projectId } = command;
|
|
12775
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12776
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "runner.provisionGhRunners", projectId, key };
|
|
12777
|
+
return await audited(actor, { operationId: "runner.provisionGhRunners", aggregateType: "project", aggregateId: projectId, projectId, data: { runnerId } }, async (policy) => {
|
|
12778
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12779
|
+
const run = await runIdempotent(idempotency, scope, { runnerId, projectId }, async () => {
|
|
12780
|
+
const row = unwrap(await write2.provisionGhRunners({ runnerId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
|
|
12781
|
+
return {
|
|
12782
|
+
projectId,
|
|
12783
|
+
runnerId,
|
|
12784
|
+
ok: row.ok,
|
|
12785
|
+
runners: row.runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error, labels: r.labels })),
|
|
12786
|
+
error: row.error
|
|
12787
|
+
};
|
|
12788
|
+
}, now());
|
|
12789
|
+
return { value: run.result, replayed: run.replayed };
|
|
12790
|
+
});
|
|
12791
|
+
},
|
|
12792
|
+
async teardownGhRunners(actor, command) {
|
|
12793
|
+
const { runnerId, projectId } = command;
|
|
12794
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12795
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "runner.teardownGhRunners", projectId, key };
|
|
12796
|
+
const repo = command.repo ?? null;
|
|
12797
|
+
return await audited(actor, {
|
|
12798
|
+
operationId: "runner.teardownGhRunners",
|
|
12799
|
+
aggregateType: "project",
|
|
12800
|
+
aggregateId: projectId,
|
|
12801
|
+
projectId,
|
|
12802
|
+
data: { runnerId, ...repo === null ? {} : { repo: `${repo.owner}/${repo.repo}` } }
|
|
12803
|
+
}, async (policy) => {
|
|
12804
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12805
|
+
const run = await runIdempotent(idempotency, scope, { runnerId, projectId, repo }, async () => {
|
|
12806
|
+
const row = unwrap(await write2.teardownGhRunners({
|
|
12807
|
+
runnerId,
|
|
12808
|
+
projectId,
|
|
12809
|
+
sealedBundle: command.sealedBundle ?? null,
|
|
12810
|
+
repo,
|
|
12811
|
+
actor: accountActorOf(actor)
|
|
12812
|
+
}));
|
|
12813
|
+
return { projectId, runnerId, ok: row.ok, error: row.error };
|
|
12814
|
+
}, now());
|
|
12815
|
+
return { value: run.result, replayed: run.replayed };
|
|
12816
|
+
});
|
|
12817
|
+
},
|
|
12818
|
+
async requestProjectIndex(actor, command) {
|
|
12819
|
+
const { projectId, kind } = command;
|
|
12820
|
+
const key = asIdempotencyKey(command.idempotencyKey);
|
|
12821
|
+
const scope = { subjectId: actor.principal.subjectId, operationId: "project.index", projectId, key };
|
|
12822
|
+
return await audited(actor, { operationId: "project.index", aggregateType: "project", aggregateId: projectId, projectId, data: { kind } }, async (policy) => {
|
|
12823
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12824
|
+
const run = await runIdempotent(idempotency, scope, { kind }, async () => {
|
|
12825
|
+
const { jobId } = unwrap(await write2.enqueueProjectIndex({ projectId, kind }));
|
|
12826
|
+
return { jobId, kind };
|
|
12827
|
+
}, now());
|
|
12828
|
+
return { value: run.result, replayed: run.replayed };
|
|
12829
|
+
});
|
|
12830
|
+
},
|
|
12831
|
+
async reportInitStatus(actor, command) {
|
|
12832
|
+
const { projectId, phase } = command;
|
|
12833
|
+
return await audited(actor, { operationId: "project.reportInitStatus", aggregateType: "project", aggregateId: projectId, projectId, data: { phase, failed: command.error !== void 0 } }, async (policy) => {
|
|
12834
|
+
await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
|
|
12835
|
+
const { recorded } = unwrap(await write2.reportInitStatus({ projectId, phase, error: command.error ?? null }));
|
|
12836
|
+
return { value: { recorded }, replayed: false };
|
|
12837
|
+
});
|
|
12838
|
+
},
|
|
12688
12839
|
async setRepoDefaultBranch(actor, command) {
|
|
12689
12840
|
const { projectId, name } = command;
|
|
12690
12841
|
const revision = asRevision(command.revision);
|
|
@@ -25849,155 +26000,8 @@ var init_sandbox2 = __esm({
|
|
|
25849
26000
|
}
|
|
25850
26001
|
});
|
|
25851
26002
|
|
|
25852
|
-
// src/farm/pool-spec.ts
|
|
25853
|
-
var pool_spec_exports = {};
|
|
25854
|
-
__export(pool_spec_exports, {
|
|
25855
|
-
buildSyncWorkerPoolMessage: () => buildSyncWorkerPoolMessage,
|
|
25856
|
-
computeFarmPoolSpec: () => computeFarmPoolSpec,
|
|
25857
|
-
reconcileFarmPools: () => reconcileFarmPools
|
|
25858
|
-
});
|
|
25859
|
-
import { nanoid as nanoid25 } from "nanoid";
|
|
25860
|
-
async function computeFarmPoolSpec(db, farmId) {
|
|
25861
|
-
const rows = await db.all(
|
|
25862
|
-
`SELECT fa.user_id as userId, fa.max_sandboxes as quota,
|
|
25863
|
-
p.id as projectId
|
|
25864
|
-
FROM farm_assignments fa
|
|
25865
|
-
JOIN projects p ON p.farm_id = fa.farm_id AND p.owner_user_id = fa.user_id
|
|
25866
|
-
WHERE fa.farm_id = ?
|
|
25867
|
-
ORDER BY fa.assigned_at ASC, p.id ASC`,
|
|
25868
|
-
farmId
|
|
25869
|
-
);
|
|
25870
|
-
const ceilingRow = await db.get(
|
|
25871
|
-
`SELECT max_sandboxes as ceiling FROM farms WHERE id = ?`,
|
|
25872
|
-
farmId
|
|
25873
|
-
);
|
|
25874
|
-
const ceiling = ceilingRow ? Number(ceilingRow.ceiling) : 16;
|
|
25875
|
-
const byAccount = /* @__PURE__ */ new Map();
|
|
25876
|
-
for (const r of rows) {
|
|
25877
|
-
const acc = byAccount.get(r.userId) ?? { quota: Number(r.quota), projectIds: [] };
|
|
25878
|
-
acc.projectIds.push(r.projectId);
|
|
25879
|
-
byAccount.set(r.userId, acc);
|
|
25880
|
-
}
|
|
25881
|
-
const spec = [];
|
|
25882
|
-
let remaining = ceiling;
|
|
25883
|
-
for (const [userId, acc] of byAccount) {
|
|
25884
|
-
const quota = Math.max(0, Math.min(acc.quota, remaining));
|
|
25885
|
-
remaining -= quota;
|
|
25886
|
-
spec.push({ userId, projectIds: acc.projectIds, quota });
|
|
25887
|
-
}
|
|
25888
|
-
return spec;
|
|
25889
|
-
}
|
|
25890
|
-
function buildSyncWorkerPoolMessage(accounts) {
|
|
25891
|
-
return { type: "sync_worker_pool", requestId: nanoid25(16), accounts };
|
|
25892
|
-
}
|
|
25893
|
-
async function reconcileFarmPools(db, send, log) {
|
|
25894
|
-
const farms = await db.all(`SELECT id FROM farms`);
|
|
25895
|
-
const live = new Set(farms.map((f) => f.id));
|
|
25896
|
-
for (const id of [...starvationReported]) {
|
|
25897
|
-
if (!live.has(id)) starvationReported.delete(id);
|
|
25898
|
-
}
|
|
25899
|
-
const starved = [];
|
|
25900
|
-
let pushed = 0;
|
|
25901
|
-
for (const farm of farms) {
|
|
25902
|
-
const accounts = await computeFarmPoolSpec(db, farm.id);
|
|
25903
|
-
if (accounts.length > 0) {
|
|
25904
|
-
const sent = send(farm.id, buildSyncWorkerPoolMessage(accounts));
|
|
25905
|
-
if (sent !== false) pushed++;
|
|
25906
|
-
starvationReported.delete(farm.id);
|
|
25907
|
-
continue;
|
|
25908
|
-
}
|
|
25909
|
-
const queued = await db.get(
|
|
25910
|
-
`SELECT COUNT(*) AS n FROM jobs j
|
|
25911
|
-
JOIN projects p ON p.id = j.project_id
|
|
25912
|
-
WHERE j.state = 'queued' AND p.farm_id = ?`,
|
|
25913
|
-
farm.id
|
|
25914
|
-
);
|
|
25915
|
-
if (Number(queued.n) === 0) {
|
|
25916
|
-
starvationReported.delete(farm.id);
|
|
25917
|
-
continue;
|
|
25918
|
-
}
|
|
25919
|
-
starved.push(farm.id);
|
|
25920
|
-
if (!starvationReported.has(farm.id)) {
|
|
25921
|
-
starvationReported.add(farm.id);
|
|
25922
|
-
log.warn(
|
|
25923
|
-
{ farmId: farm.id, queued: Number(queued.n) },
|
|
25924
|
-
"farm has NO worker-pool accounts while jobs are queued \u2014 assignments missing?"
|
|
25925
|
-
);
|
|
25926
|
-
}
|
|
25927
|
-
}
|
|
25928
|
-
return { farms: farms.length, pushed, starved };
|
|
25929
|
-
}
|
|
25930
|
-
var starvationReported;
|
|
25931
|
-
var init_pool_spec = __esm({
|
|
25932
|
-
"src/farm/pool-spec.ts"() {
|
|
25933
|
-
"use strict";
|
|
25934
|
-
starvationReported = /* @__PURE__ */ new Set();
|
|
25935
|
-
}
|
|
25936
|
-
});
|
|
25937
|
-
|
|
25938
|
-
// src/farm/provision.ts
|
|
25939
|
-
import { nanoid as nanoid26 } from "nanoid";
|
|
25940
|
-
function handleFarmResult(msg) {
|
|
25941
|
-
const type = msg["type"];
|
|
25942
|
-
if (typeof type !== "string" || !RESULT_TYPES.has(type)) return;
|
|
25943
|
-
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
25944
|
-
const p = pending2.get(requestId);
|
|
25945
|
-
if (!p) return;
|
|
25946
|
-
clearTimeout(p.timer);
|
|
25947
|
-
pending2.delete(requestId);
|
|
25948
|
-
p.resolve(msg);
|
|
25949
|
-
}
|
|
25950
|
-
function awaitResult(requestId, send, timeoutMs) {
|
|
25951
|
-
if (!send()) return Promise.reject(new Error("farm_offline"));
|
|
25952
|
-
return new Promise((resolve4, reject) => {
|
|
25953
|
-
const timer = setTimeout(() => {
|
|
25954
|
-
pending2.delete(requestId);
|
|
25955
|
-
reject(new Error("farm_timeout"));
|
|
25956
|
-
}, timeoutMs);
|
|
25957
|
-
pending2.set(requestId, { resolve: resolve4, reject, timer });
|
|
25958
|
-
});
|
|
25959
|
-
}
|
|
25960
|
-
function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
|
|
25961
|
-
const requestId = nanoid26(16);
|
|
25962
|
-
return awaitResult(
|
|
25963
|
-
requestId,
|
|
25964
|
-
() => sendToFarm(farmId, { type: "provision_project", requestId, projectId, sealedBundle }),
|
|
25965
|
-
timeoutMs
|
|
25966
|
-
);
|
|
25967
|
-
}
|
|
25968
|
-
function requestDelete(farmId, projectId, timeoutMs) {
|
|
25969
|
-
const requestId = nanoid26(16);
|
|
25970
|
-
return awaitResult(
|
|
25971
|
-
requestId,
|
|
25972
|
-
() => sendToFarm(farmId, { type: "delete_project", requestId, projectId }),
|
|
25973
|
-
timeoutMs
|
|
25974
|
-
);
|
|
25975
|
-
}
|
|
25976
|
-
function requestRespawn(farmId, projectId, timeoutMs) {
|
|
25977
|
-
const requestId = nanoid26(16);
|
|
25978
|
-
return awaitResult(
|
|
25979
|
-
requestId,
|
|
25980
|
-
() => sendToFarm(farmId, { type: "respawn_project", requestId, projectId }),
|
|
25981
|
-
timeoutMs
|
|
25982
|
-
);
|
|
25983
|
-
}
|
|
25984
|
-
async function requestFarmPoolSync(db, farmId) {
|
|
25985
|
-
const accounts = await computeFarmPoolSpec(db, farmId);
|
|
25986
|
-
sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
|
|
25987
|
-
}
|
|
25988
|
-
var pending2, RESULT_TYPES;
|
|
25989
|
-
var init_provision = __esm({
|
|
25990
|
-
"src/farm/provision.ts"() {
|
|
25991
|
-
"use strict";
|
|
25992
|
-
init_ws();
|
|
25993
|
-
init_pool_spec();
|
|
25994
|
-
pending2 = /* @__PURE__ */ new Map();
|
|
25995
|
-
RESULT_TYPES = /* @__PURE__ */ new Set(["provision_result", "delete_result", "respawn_result"]);
|
|
25996
|
-
}
|
|
25997
|
-
});
|
|
25998
|
-
|
|
25999
26003
|
// src/routes/farm.ts
|
|
26000
|
-
import { nanoid as
|
|
26004
|
+
import { nanoid as nanoid25 } from "nanoid";
|
|
26001
26005
|
function viewerInAssignments(viewer, assignments) {
|
|
26002
26006
|
return assignments.some(
|
|
26003
26007
|
(a) => viewer.userId !== null && a.userId === viewer.userId || a.email !== null && a.email === viewer.email
|
|
@@ -26082,7 +26086,7 @@ async function farmRoutes(app, deps) {
|
|
|
26082
26086
|
if (req.body.registrationToken !== config.farmRegistrationToken) {
|
|
26083
26087
|
return reply.status(401).send({ error: "bad_registration_token" });
|
|
26084
26088
|
}
|
|
26085
|
-
const token =
|
|
26089
|
+
const token = nanoid25(48);
|
|
26086
26090
|
const now = Date.now();
|
|
26087
26091
|
const publicKey = req.body.publicKey ?? null;
|
|
26088
26092
|
const fingerprint2 = publicKey ? publicKeyFingerprint(publicKey) : null;
|
|
@@ -26121,7 +26125,7 @@ async function farmRoutes(app, deps) {
|
|
|
26121
26125
|
"farm registered under an existing name with a DIFFERENT key \u2014 new identity; assignments stay with the old farm and must be moved deliberately"
|
|
26122
26126
|
);
|
|
26123
26127
|
}
|
|
26124
|
-
const id = `farm_${
|
|
26128
|
+
const id = `farm_${nanoid25(16)}`;
|
|
26125
26129
|
await db.run(`INSERT INTO farms
|
|
26126
26130
|
(id, name, token, capabilities_json, public_key, key_fingerprint, registered_at, last_seen_at, max_sandboxes)
|
|
26127
26131
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, req.body.name, token, JSON.stringify(req.body.capabilities), publicKey, fingerprint2, now, now, maxSandboxes);
|
|
@@ -26129,104 +26133,25 @@ async function farmRoutes(app, deps) {
|
|
|
26129
26133
|
return { farmId: id, token };
|
|
26130
26134
|
}
|
|
26131
26135
|
);
|
|
26132
|
-
app.get("/farms", async (req, reply) => {
|
|
26133
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
26134
|
-
if (auth) {
|
|
26135
|
-
return await listFarms(db, {
|
|
26136
|
-
email: auth.email,
|
|
26137
|
-
userId: auth.userId,
|
|
26138
|
-
isSuperAdmin: isSuperUser(config, auth.email)
|
|
26139
|
-
});
|
|
26140
|
-
}
|
|
26141
|
-
const session = await requireSession(db, req, reply);
|
|
26142
|
-
if (!session) return reply;
|
|
26143
|
-
return await listFarms(db, {
|
|
26144
|
-
email: session.email,
|
|
26145
|
-
userId: session.userId,
|
|
26146
|
-
isSuperAdmin: isSuperUser(config, session.email)
|
|
26147
|
-
});
|
|
26148
|
-
});
|
|
26149
|
-
app.post(
|
|
26150
|
-
"/farms/:farmId/provision",
|
|
26151
|
-
{
|
|
26152
|
-
schema: {
|
|
26153
|
-
body: {
|
|
26154
|
-
type: "object",
|
|
26155
|
-
required: ["projectId", "sealedBundle"],
|
|
26156
|
-
properties: {
|
|
26157
|
-
projectId: { type: "string" },
|
|
26158
|
-
sealedBundle: { type: "string", minLength: 1 }
|
|
26159
|
-
}
|
|
26160
|
-
}
|
|
26161
|
-
}
|
|
26162
|
-
},
|
|
26163
|
-
async (req, reply) => {
|
|
26164
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
26165
|
-
if (!auth) {
|
|
26166
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
26167
|
-
}
|
|
26168
|
-
const { farmId } = req.params;
|
|
26169
|
-
const { projectId, sealedBundle } = req.body;
|
|
26170
|
-
if (!await canActOnProject(db, config, auth, projectId)) {
|
|
26171
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
26172
|
-
}
|
|
26173
|
-
const farm = await db.get(`SELECT id FROM farms WHERE id = ?`, farmId);
|
|
26174
|
-
if (!farm) return reply.status(404).send({ error: "unknown_farm" });
|
|
26175
|
-
if (!isSuperUser(config, auth.email) && !await viewerOwnsFarm(db, { email: auth.email, userId: auth.userId }, farmId)) {
|
|
26176
|
-
return reply.status(403).send({ error: "farm_not_assigned" });
|
|
26177
|
-
}
|
|
26178
|
-
if (!await db.get(`SELECT id FROM projects WHERE id = ?`, projectId)) {
|
|
26179
|
-
return reply.status(404).send({ error: "unknown_project" });
|
|
26180
|
-
}
|
|
26181
|
-
await beginInitTracking(db, projectId, "farm_provisioning");
|
|
26182
|
-
try {
|
|
26183
|
-
const result = await requestProvision(farmId, projectId, sealedBundle, 6e4);
|
|
26184
|
-
if (result.ok) {
|
|
26185
|
-
await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
|
|
26186
|
-
await advanceInitPhase(db, projectId, "daemon_starting");
|
|
26187
|
-
requestFarmPoolSync(db, farmId).catch(
|
|
26188
|
-
(err) => app.log.warn({ farmId, err }, "farm pool sync after provision failed")
|
|
26189
|
-
);
|
|
26190
|
-
} else {
|
|
26191
|
-
await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
|
|
26192
|
-
}
|
|
26193
|
-
return result;
|
|
26194
|
-
} catch (err) {
|
|
26195
|
-
const reason = err instanceof Error ? err.message : "provision_failed";
|
|
26196
|
-
await recordInitError(
|
|
26197
|
-
db,
|
|
26198
|
-
projectId,
|
|
26199
|
-
reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
|
|
26200
|
-
);
|
|
26201
|
-
const status = reason === "farm_offline" ? 409 : reason === "farm_timeout" ? 504 : 500;
|
|
26202
|
-
return reply.status(status).send({ error: reason });
|
|
26203
|
-
}
|
|
26204
|
-
}
|
|
26205
|
-
);
|
|
26206
26136
|
}
|
|
26207
26137
|
var init_farm2 = __esm({
|
|
26208
26138
|
"src/routes/farm.ts"() {
|
|
26209
26139
|
"use strict";
|
|
26210
26140
|
init_sealedbox();
|
|
26211
26141
|
init_abuse_throttle();
|
|
26212
|
-
init_cli_auth();
|
|
26213
|
-
init_auth();
|
|
26214
|
-
init_users();
|
|
26215
|
-
init_init_status();
|
|
26216
26142
|
init_ws();
|
|
26217
|
-
init_provision();
|
|
26218
26143
|
}
|
|
26219
26144
|
});
|
|
26220
26145
|
|
|
26221
26146
|
// src/runner-reg-tokens.ts
|
|
26222
26147
|
import { createHash as createHash8 } from "node:crypto";
|
|
26223
|
-
import { nanoid as
|
|
26148
|
+
import { nanoid as nanoid26 } from "nanoid";
|
|
26224
26149
|
function hashRunnerRegToken(secret) {
|
|
26225
26150
|
return createHash8("sha256").update(secret, "utf8").digest("hex");
|
|
26226
26151
|
}
|
|
26227
26152
|
async function issueRunnerRegToken(db, email) {
|
|
26228
26153
|
const normalized = email.trim().toLowerCase();
|
|
26229
|
-
const secret = `rnrreg_${
|
|
26154
|
+
const secret = `rnrreg_${nanoid26(48)}`;
|
|
26230
26155
|
await db.tx(async (db2) => {
|
|
26231
26156
|
await db2.run(`UPDATE runner_reg_tokens SET revoked_at = ? WHERE email = ? AND revoked_at IS NULL`, Date.now(), normalized);
|
|
26232
26157
|
await db2.run(`INSERT INTO runner_reg_tokens (token_hash, email, user_id, created_at)
|
|
@@ -26262,130 +26187,8 @@ var init_runner_reg_tokens = __esm({
|
|
|
26262
26187
|
}
|
|
26263
26188
|
});
|
|
26264
26189
|
|
|
26265
|
-
// src/runner/provision.ts
|
|
26266
|
-
import { nanoid as nanoid29 } from "nanoid";
|
|
26267
|
-
function handleRunnerResult(msg) {
|
|
26268
|
-
const type = msg["type"];
|
|
26269
|
-
if (typeof type !== "string" || !RESULT_TYPES2.has(type)) return;
|
|
26270
|
-
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
26271
|
-
const p = pending3.get(requestId);
|
|
26272
|
-
if (!p) return;
|
|
26273
|
-
clearTimeout(p.timer);
|
|
26274
|
-
pending3.delete(requestId);
|
|
26275
|
-
p.resolve(msg);
|
|
26276
|
-
}
|
|
26277
|
-
function awaitResult2(requestId, send, timeoutMs) {
|
|
26278
|
-
if (!send()) return Promise.reject(new Error("runner_offline"));
|
|
26279
|
-
return new Promise((resolve4, reject) => {
|
|
26280
|
-
const timer = setTimeout(() => {
|
|
26281
|
-
pending3.delete(requestId);
|
|
26282
|
-
reject(new Error("runner_timeout"));
|
|
26283
|
-
}, timeoutMs);
|
|
26284
|
-
pending3.set(requestId, { resolve: resolve4, reject, timer });
|
|
26285
|
-
});
|
|
26286
|
-
}
|
|
26287
|
-
function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
26288
|
-
const requestId = nanoid29(16);
|
|
26289
|
-
return awaitResult2(
|
|
26290
|
-
requestId,
|
|
26291
|
-
() => sendToRunner(runnerId, { type: "provision_gh_runners", requestId, projectId, sealedBundle }),
|
|
26292
|
-
timeoutMs
|
|
26293
|
-
);
|
|
26294
|
-
}
|
|
26295
|
-
function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
26296
|
-
const requestId = nanoid29(16);
|
|
26297
|
-
return awaitResult2(
|
|
26298
|
-
requestId,
|
|
26299
|
-
() => sendToRunner(runnerId, {
|
|
26300
|
-
type: "delete_gh_runners",
|
|
26301
|
-
requestId,
|
|
26302
|
-
projectId,
|
|
26303
|
-
...sealedBundle ? { sealedBundle } : {}
|
|
26304
|
-
}),
|
|
26305
|
-
timeoutMs
|
|
26306
|
-
);
|
|
26307
|
-
}
|
|
26308
|
-
var pending3, RESULT_TYPES2;
|
|
26309
|
-
var init_provision2 = __esm({
|
|
26310
|
-
"src/runner/provision.ts"() {
|
|
26311
|
-
"use strict";
|
|
26312
|
-
init_ws();
|
|
26313
|
-
pending3 = /* @__PURE__ */ new Map();
|
|
26314
|
-
RESULT_TYPES2 = /* @__PURE__ */ new Set(["provision_gh_runners_result", "delete_gh_runners_result"]);
|
|
26315
|
-
}
|
|
26316
|
-
});
|
|
26317
|
-
|
|
26318
|
-
// src/runner/bindings.ts
|
|
26319
|
-
function parseGhRunnerBindings(json) {
|
|
26320
|
-
if (!json) return [];
|
|
26321
|
-
try {
|
|
26322
|
-
const parsed = JSON.parse(json);
|
|
26323
|
-
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
26324
|
-
} catch {
|
|
26325
|
-
return [];
|
|
26326
|
-
}
|
|
26327
|
-
}
|
|
26328
|
-
function serialize(bindings) {
|
|
26329
|
-
return JSON.stringify({ bindings });
|
|
26330
|
-
}
|
|
26331
|
-
async function upsertGhRunnerBinding(db, projectId, binding) {
|
|
26332
|
-
await db.tx(async (tx) => {
|
|
26333
|
-
const row = await tx.get(
|
|
26334
|
-
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
26335
|
-
projectId
|
|
26336
|
-
);
|
|
26337
|
-
if (!row) return;
|
|
26338
|
-
const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
|
|
26339
|
-
(b) => !(b.owner === binding.owner && b.repo === binding.repo)
|
|
26340
|
-
);
|
|
26341
|
-
bindings.push(binding);
|
|
26342
|
-
await tx.run(
|
|
26343
|
-
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
26344
|
-
serialize(bindings),
|
|
26345
|
-
projectId
|
|
26346
|
-
);
|
|
26347
|
-
});
|
|
26348
|
-
}
|
|
26349
|
-
async function removeGhRunnerBinding(db, projectId, owner, repo) {
|
|
26350
|
-
return db.tx(async (tx) => {
|
|
26351
|
-
const row = await tx.get(
|
|
26352
|
-
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
26353
|
-
projectId
|
|
26354
|
-
);
|
|
26355
|
-
if (!row) return false;
|
|
26356
|
-
const bindings = parseGhRunnerBindings(row.bindingsJson);
|
|
26357
|
-
const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
|
|
26358
|
-
if (kept.length === bindings.length) return false;
|
|
26359
|
-
await tx.run(
|
|
26360
|
-
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
26361
|
-
serialize(kept),
|
|
26362
|
-
projectId
|
|
26363
|
-
);
|
|
26364
|
-
return true;
|
|
26365
|
-
});
|
|
26366
|
-
}
|
|
26367
|
-
async function runnerBoundProjectCount(db, runnerId) {
|
|
26368
|
-
const rows = await db.all(
|
|
26369
|
-
`SELECT gh_runner_bindings_json AS json FROM projects WHERE gh_runner_bindings_json IS NOT NULL`
|
|
26370
|
-
);
|
|
26371
|
-
let n = 0;
|
|
26372
|
-
for (const r of rows) {
|
|
26373
|
-
try {
|
|
26374
|
-
const bindings = JSON.parse(r.json).bindings ?? [];
|
|
26375
|
-
if (bindings.some((b) => b.runnerId === runnerId)) n++;
|
|
26376
|
-
} catch {
|
|
26377
|
-
}
|
|
26378
|
-
}
|
|
26379
|
-
return n;
|
|
26380
|
-
}
|
|
26381
|
-
var init_bindings = __esm({
|
|
26382
|
-
"src/runner/bindings.ts"() {
|
|
26383
|
-
"use strict";
|
|
26384
|
-
}
|
|
26385
|
-
});
|
|
26386
|
-
|
|
26387
26190
|
// src/routes/runner.ts
|
|
26388
|
-
import { nanoid as
|
|
26191
|
+
import { nanoid as nanoid27 } from "nanoid";
|
|
26389
26192
|
async function listRunners(db, viewer) {
|
|
26390
26193
|
const online = new Set(listConnectedRunnerIds());
|
|
26391
26194
|
const rows = await db.all(`SELECT id, name, public_key as publicKey,
|
|
@@ -26462,8 +26265,8 @@ async function runnerRoutes(app, deps) {
|
|
|
26462
26265
|
if (!owner) {
|
|
26463
26266
|
return reply.status(401).send({ error: "bad_registration_token" });
|
|
26464
26267
|
}
|
|
26465
|
-
const id = `rnr_${
|
|
26466
|
-
const token =
|
|
26268
|
+
const id = `rnr_${nanoid27(16)}`;
|
|
26269
|
+
const token = nanoid27(48);
|
|
26467
26270
|
const now = Date.now();
|
|
26468
26271
|
const publicKey = req.body.publicKey ?? null;
|
|
26469
26272
|
const fingerprint2 = publicKey ? publicKeyFingerprint(publicKey) : null;
|
|
@@ -26531,108 +26334,6 @@ async function runnerRoutes(app, deps) {
|
|
|
26531
26334
|
return { ok: true };
|
|
26532
26335
|
}
|
|
26533
26336
|
);
|
|
26534
|
-
const ghRunnerGuard = async (authorization, reply, runnerId, projectId) => {
|
|
26535
|
-
const auth = await resolveCliAuth(db, config, authorization);
|
|
26536
|
-
if (!auth) {
|
|
26537
|
-
reply.status(401).send({ error: "unauthorized" });
|
|
26538
|
-
return null;
|
|
26539
|
-
}
|
|
26540
|
-
if (!await canActOnProject(db, config, auth, projectId)) {
|
|
26541
|
-
reply.status(403).send({ error: "forbidden" });
|
|
26542
|
-
return null;
|
|
26543
|
-
}
|
|
26544
|
-
if (!await db.get(`SELECT id FROM runners WHERE id = ?`, runnerId)) {
|
|
26545
|
-
reply.status(404).send({ error: "unknown_runner" });
|
|
26546
|
-
return null;
|
|
26547
|
-
}
|
|
26548
|
-
if (!isSuperUser(config, auth.email) && !await viewerOwnsRunner(db, { email: auth.email, userId: auth.userId }, runnerId)) {
|
|
26549
|
-
reply.status(403).send({ error: "runner_not_owned" });
|
|
26550
|
-
return null;
|
|
26551
|
-
}
|
|
26552
|
-
if (!await db.get(`SELECT id FROM projects WHERE id = ?`, projectId)) {
|
|
26553
|
-
reply.status(404).send({ error: "unknown_project" });
|
|
26554
|
-
return null;
|
|
26555
|
-
}
|
|
26556
|
-
return auth;
|
|
26557
|
-
};
|
|
26558
|
-
const ghRelayFailure = (reply, err) => {
|
|
26559
|
-
const reason = err instanceof Error ? err.message : "gh_runner_relay_failed";
|
|
26560
|
-
const status = reason === "runner_offline" ? 409 : reason === "runner_timeout" ? 504 : 500;
|
|
26561
|
-
return reply.status(status).send({ error: reason });
|
|
26562
|
-
};
|
|
26563
|
-
app.post(
|
|
26564
|
-
"/runners/:runnerId/gh-runners",
|
|
26565
|
-
{
|
|
26566
|
-
schema: {
|
|
26567
|
-
body: {
|
|
26568
|
-
type: "object",
|
|
26569
|
-
required: ["projectId", "sealedBundle"],
|
|
26570
|
-
properties: {
|
|
26571
|
-
projectId: { type: "string" },
|
|
26572
|
-
sealedBundle: { type: "string", minLength: 1 }
|
|
26573
|
-
}
|
|
26574
|
-
}
|
|
26575
|
-
}
|
|
26576
|
-
},
|
|
26577
|
-
async (req, reply) => {
|
|
26578
|
-
const { runnerId } = req.params;
|
|
26579
|
-
const { projectId, sealedBundle } = req.body;
|
|
26580
|
-
if (!await ghRunnerGuard(req.headers.authorization, reply, runnerId, projectId)) return reply;
|
|
26581
|
-
try {
|
|
26582
|
-
const result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, 12e4);
|
|
26583
|
-
if (result.ok) {
|
|
26584
|
-
const connectedAt = Date.now();
|
|
26585
|
-
for (const r of result.runners ?? []) {
|
|
26586
|
-
await upsertGhRunnerBinding(db, projectId, {
|
|
26587
|
-
owner: r.owner,
|
|
26588
|
-
repo: r.repo,
|
|
26589
|
-
runnerId,
|
|
26590
|
-
connectedAt,
|
|
26591
|
-
status: r.status,
|
|
26592
|
-
...r.error ? { error: r.error } : {},
|
|
26593
|
-
...r.labels ? { labels: r.labels } : {}
|
|
26594
|
-
});
|
|
26595
|
-
}
|
|
26596
|
-
}
|
|
26597
|
-
return result;
|
|
26598
|
-
} catch (err) {
|
|
26599
|
-
return ghRelayFailure(reply, err);
|
|
26600
|
-
}
|
|
26601
|
-
}
|
|
26602
|
-
);
|
|
26603
|
-
app.post(
|
|
26604
|
-
"/runners/:runnerId/gh-runners/teardown",
|
|
26605
|
-
{
|
|
26606
|
-
schema: {
|
|
26607
|
-
body: {
|
|
26608
|
-
type: "object",
|
|
26609
|
-
required: ["projectId"],
|
|
26610
|
-
properties: {
|
|
26611
|
-
projectId: { type: "string" },
|
|
26612
|
-
sealedBundle: { type: "string", minLength: 1 },
|
|
26613
|
-
// DEV-530: the one repo being disconnected — plaintext public
|
|
26614
|
-
// identifiers (not secrets), so the HUB can drop its binding.
|
|
26615
|
-
owner: { type: "string", minLength: 1 },
|
|
26616
|
-
repo: { type: "string", minLength: 1 }
|
|
26617
|
-
}
|
|
26618
|
-
}
|
|
26619
|
-
}
|
|
26620
|
-
},
|
|
26621
|
-
async (req, reply) => {
|
|
26622
|
-
const { runnerId } = req.params;
|
|
26623
|
-
const { projectId, sealedBundle, owner, repo } = req.body;
|
|
26624
|
-
if (!await ghRunnerGuard(req.headers.authorization, reply, runnerId, projectId)) return reply;
|
|
26625
|
-
try {
|
|
26626
|
-
const result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle, 12e4);
|
|
26627
|
-
if (result.ok && owner && repo) {
|
|
26628
|
-
await removeGhRunnerBinding(db, projectId, owner, repo);
|
|
26629
|
-
}
|
|
26630
|
-
return result;
|
|
26631
|
-
} catch (err) {
|
|
26632
|
-
return ghRelayFailure(reply, err);
|
|
26633
|
-
}
|
|
26634
|
-
}
|
|
26635
|
-
);
|
|
26636
26337
|
}
|
|
26637
26338
|
var init_runner2 = __esm({
|
|
26638
26339
|
"src/routes/runner.ts"() {
|
|
@@ -26644,8 +26345,92 @@ var init_runner2 = __esm({
|
|
|
26644
26345
|
init_users();
|
|
26645
26346
|
init_runner_reg_tokens();
|
|
26646
26347
|
init_ws();
|
|
26647
|
-
|
|
26648
|
-
|
|
26348
|
+
}
|
|
26349
|
+
});
|
|
26350
|
+
|
|
26351
|
+
// src/farm/pool-spec.ts
|
|
26352
|
+
var pool_spec_exports = {};
|
|
26353
|
+
__export(pool_spec_exports, {
|
|
26354
|
+
buildSyncWorkerPoolMessage: () => buildSyncWorkerPoolMessage,
|
|
26355
|
+
computeFarmPoolSpec: () => computeFarmPoolSpec,
|
|
26356
|
+
reconcileFarmPools: () => reconcileFarmPools
|
|
26357
|
+
});
|
|
26358
|
+
import { nanoid as nanoid28 } from "nanoid";
|
|
26359
|
+
async function computeFarmPoolSpec(db, farmId) {
|
|
26360
|
+
const rows = await db.all(
|
|
26361
|
+
`SELECT fa.user_id as userId, fa.max_sandboxes as quota,
|
|
26362
|
+
p.id as projectId
|
|
26363
|
+
FROM farm_assignments fa
|
|
26364
|
+
JOIN projects p ON p.farm_id = fa.farm_id AND p.owner_user_id = fa.user_id
|
|
26365
|
+
WHERE fa.farm_id = ?
|
|
26366
|
+
ORDER BY fa.assigned_at ASC, p.id ASC`,
|
|
26367
|
+
farmId
|
|
26368
|
+
);
|
|
26369
|
+
const ceilingRow = await db.get(
|
|
26370
|
+
`SELECT max_sandboxes as ceiling FROM farms WHERE id = ?`,
|
|
26371
|
+
farmId
|
|
26372
|
+
);
|
|
26373
|
+
const ceiling = ceilingRow ? Number(ceilingRow.ceiling) : 16;
|
|
26374
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
26375
|
+
for (const r of rows) {
|
|
26376
|
+
const acc = byAccount.get(r.userId) ?? { quota: Number(r.quota), projectIds: [] };
|
|
26377
|
+
acc.projectIds.push(r.projectId);
|
|
26378
|
+
byAccount.set(r.userId, acc);
|
|
26379
|
+
}
|
|
26380
|
+
const spec = [];
|
|
26381
|
+
let remaining = ceiling;
|
|
26382
|
+
for (const [userId, acc] of byAccount) {
|
|
26383
|
+
const quota = Math.max(0, Math.min(acc.quota, remaining));
|
|
26384
|
+
remaining -= quota;
|
|
26385
|
+
spec.push({ userId, projectIds: acc.projectIds, quota });
|
|
26386
|
+
}
|
|
26387
|
+
return spec;
|
|
26388
|
+
}
|
|
26389
|
+
function buildSyncWorkerPoolMessage(accounts) {
|
|
26390
|
+
return { type: "sync_worker_pool", requestId: nanoid28(16), accounts };
|
|
26391
|
+
}
|
|
26392
|
+
async function reconcileFarmPools(db, send, log) {
|
|
26393
|
+
const farms = await db.all(`SELECT id FROM farms`);
|
|
26394
|
+
const live = new Set(farms.map((f) => f.id));
|
|
26395
|
+
for (const id of [...starvationReported]) {
|
|
26396
|
+
if (!live.has(id)) starvationReported.delete(id);
|
|
26397
|
+
}
|
|
26398
|
+
const starved = [];
|
|
26399
|
+
let pushed = 0;
|
|
26400
|
+
for (const farm of farms) {
|
|
26401
|
+
const accounts = await computeFarmPoolSpec(db, farm.id);
|
|
26402
|
+
if (accounts.length > 0) {
|
|
26403
|
+
const sent = send(farm.id, buildSyncWorkerPoolMessage(accounts));
|
|
26404
|
+
if (sent !== false) pushed++;
|
|
26405
|
+
starvationReported.delete(farm.id);
|
|
26406
|
+
continue;
|
|
26407
|
+
}
|
|
26408
|
+
const queued = await db.get(
|
|
26409
|
+
`SELECT COUNT(*) AS n FROM jobs j
|
|
26410
|
+
JOIN projects p ON p.id = j.project_id
|
|
26411
|
+
WHERE j.state = 'queued' AND p.farm_id = ?`,
|
|
26412
|
+
farm.id
|
|
26413
|
+
);
|
|
26414
|
+
if (Number(queued.n) === 0) {
|
|
26415
|
+
starvationReported.delete(farm.id);
|
|
26416
|
+
continue;
|
|
26417
|
+
}
|
|
26418
|
+
starved.push(farm.id);
|
|
26419
|
+
if (!starvationReported.has(farm.id)) {
|
|
26420
|
+
starvationReported.add(farm.id);
|
|
26421
|
+
log.warn(
|
|
26422
|
+
{ farmId: farm.id, queued: Number(queued.n) },
|
|
26423
|
+
"farm has NO worker-pool accounts while jobs are queued \u2014 assignments missing?"
|
|
26424
|
+
);
|
|
26425
|
+
}
|
|
26426
|
+
}
|
|
26427
|
+
return { farms: farms.length, pushed, starved };
|
|
26428
|
+
}
|
|
26429
|
+
var starvationReported;
|
|
26430
|
+
var init_pool_spec = __esm({
|
|
26431
|
+
"src/farm/pool-spec.ts"() {
|
|
26432
|
+
"use strict";
|
|
26433
|
+
starvationReported = /* @__PURE__ */ new Set();
|
|
26649
26434
|
}
|
|
26650
26435
|
});
|
|
26651
26436
|
|
|
@@ -26905,25 +26690,25 @@ async function requestGrantsFrom(spec, recipient, bindingVersion, log, timeoutMs
|
|
|
26905
26690
|
const requestId = msg.requestId;
|
|
26906
26691
|
return await new Promise((resolve4) => {
|
|
26907
26692
|
const timer = setTimeout(() => {
|
|
26908
|
-
|
|
26693
|
+
pending2.delete(requestId);
|
|
26909
26694
|
log.warn({ projectId: spec.projectId, jobId: spec.jobId, sandboxId: recipient.sandboxId }, "credential grants: CLI did not answer in time");
|
|
26910
26695
|
resolve4(null);
|
|
26911
26696
|
}, timeoutMs);
|
|
26912
|
-
|
|
26697
|
+
pending2.set(requestId, {
|
|
26913
26698
|
projectId: spec.projectId,
|
|
26914
26699
|
jobId: spec.jobId,
|
|
26915
26700
|
sandboxId: recipient.sandboxId,
|
|
26916
26701
|
holder,
|
|
26917
26702
|
resolve: (answer) => {
|
|
26918
26703
|
clearTimeout(timer);
|
|
26919
|
-
|
|
26704
|
+
pending2.delete(requestId);
|
|
26920
26705
|
resolve4(answer);
|
|
26921
26706
|
}
|
|
26922
26707
|
});
|
|
26923
26708
|
const sent = holder.kind === "provider" ? sendToAgentLoginProvider(holder.ownerKey, holder.providerId, msg) : sendToProjectCli(spec.projectId, msg);
|
|
26924
26709
|
if (!sent) {
|
|
26925
26710
|
clearTimeout(timer);
|
|
26926
|
-
|
|
26711
|
+
pending2.delete(requestId);
|
|
26927
26712
|
resolve4(null);
|
|
26928
26713
|
}
|
|
26929
26714
|
});
|
|
@@ -26934,7 +26719,7 @@ function acceptCredentialGrants(msg, from) {
|
|
|
26934
26719
|
return { ok: false, reason: "malformed" };
|
|
26935
26720
|
}
|
|
26936
26721
|
if (typeof from === "string" && m.projectId !== from) return { ok: false, reason: "cross-project" };
|
|
26937
|
-
const req =
|
|
26722
|
+
const req = pending2.get(m.requestId);
|
|
26938
26723
|
if (!req) return { ok: false, reason: NO_PENDING_REQUEST };
|
|
26939
26724
|
if (typeof from === "string") {
|
|
26940
26725
|
if (req.holder.kind !== "client") return { ok: false, reason: "answer from a holder that was not asked" };
|
|
@@ -27140,15 +26925,15 @@ async function sweepExpiredGrants(db, now = Date.now()) {
|
|
|
27140
26925
|
return changes;
|
|
27141
26926
|
}
|
|
27142
26927
|
function resolvePendingGrantRequest(requestId, answer) {
|
|
27143
|
-
const req =
|
|
26928
|
+
const req = pending2.get(requestId);
|
|
27144
26929
|
if (!req) return false;
|
|
27145
26930
|
req.resolve(answer);
|
|
27146
26931
|
return true;
|
|
27147
26932
|
}
|
|
27148
26933
|
function pendingGrantRequests() {
|
|
27149
|
-
return
|
|
26934
|
+
return pending2.size;
|
|
27150
26935
|
}
|
|
27151
|
-
var GRANT_REQUEST_TIMEOUT_MS,
|
|
26936
|
+
var GRANT_REQUEST_TIMEOUT_MS, pending2, NO_PENDING_REQUEST, LATE_ANSWER_TTL_MS, lateAnswers;
|
|
27152
26937
|
var init_credential_grants = __esm({
|
|
27153
26938
|
"src/tracker/credential-grants.ts"() {
|
|
27154
26939
|
"use strict";
|
|
@@ -27159,7 +26944,7 @@ var init_credential_grants = __esm({
|
|
|
27159
26944
|
init_ws();
|
|
27160
26945
|
init_sandbox_keys();
|
|
27161
26946
|
GRANT_REQUEST_TIMEOUT_MS = 6e3;
|
|
27162
|
-
|
|
26947
|
+
pending2 = /* @__PURE__ */ new Map();
|
|
27163
26948
|
NO_PENDING_REQUEST = "no pending request";
|
|
27164
26949
|
LATE_ANSWER_TTL_MS = 6e4;
|
|
27165
26950
|
lateAnswers = /* @__PURE__ */ new Map();
|
|
@@ -29078,7 +28863,7 @@ function installOfflineSweeper(deps) {
|
|
|
29078
28863
|
init_dist5();
|
|
29079
28864
|
import { existsSync as existsSync4 } from "node:fs";
|
|
29080
28865
|
import staticPlugin from "@fastify/static";
|
|
29081
|
-
import { nanoid as
|
|
28866
|
+
import { nanoid as nanoid29 } from "nanoid";
|
|
29082
28867
|
|
|
29083
28868
|
// src/http/browser-routes.ts
|
|
29084
28869
|
init_route_definition();
|
|
@@ -29239,7 +29024,7 @@ async function registerSpaHosts(app, opts) {
|
|
|
29239
29024
|
return onConsole ? reply.sendFile("index.html", adminDist) : reply.sendFile("index.html");
|
|
29240
29025
|
}
|
|
29241
29026
|
const failure = isRetiredClientPath(req.url, opts.registeredPaths ?? []) ? clientTooOldError() : errors.notFound();
|
|
29242
|
-
const rendered = renderError(failure, `req_${
|
|
29027
|
+
const rendered = renderError(failure, `req_${nanoid29(16)}`);
|
|
29243
29028
|
return reply.status(rendered.status).send(rendered.body);
|
|
29244
29029
|
});
|
|
29245
29030
|
}
|
|
@@ -29253,7 +29038,7 @@ import { createHash as createHash9, randomBytes as randomBytes3 } from "node:cry
|
|
|
29253
29038
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
29254
29039
|
import { join as join4 } from "node:path";
|
|
29255
29040
|
init_dist5();
|
|
29256
|
-
import { nanoid as
|
|
29041
|
+
import { nanoid as nanoid30 } from "nanoid";
|
|
29257
29042
|
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
29258
29043
|
var nonces = /* @__PURE__ */ new WeakMap();
|
|
29259
29044
|
function cspNonce(req) {
|
|
@@ -29342,7 +29127,7 @@ function installBrowserSecurity(app, config, options = {}) {
|
|
|
29342
29127
|
message: "This request did not come from an allowed origin.",
|
|
29343
29128
|
details: { reason: verdict.reason }
|
|
29344
29129
|
}),
|
|
29345
|
-
`req_${
|
|
29130
|
+
`req_${nanoid30(16)}`
|
|
29346
29131
|
);
|
|
29347
29132
|
await reply.status(rendered.status).send(rendered.body);
|
|
29348
29133
|
});
|
|
@@ -29470,7 +29255,7 @@ init_route_registry();
|
|
|
29470
29255
|
|
|
29471
29256
|
// src/routes/api-v1/context.ts
|
|
29472
29257
|
init_dist5();
|
|
29473
|
-
import { nanoid as
|
|
29258
|
+
import { nanoid as nanoid31 } from "nanoid";
|
|
29474
29259
|
|
|
29475
29260
|
// src/auth/resolve-principal.ts
|
|
29476
29261
|
init_auth();
|
|
@@ -29596,7 +29381,7 @@ async function resolveSession(db, req, surface, base) {
|
|
|
29596
29381
|
|
|
29597
29382
|
// src/routes/api-v1/context.ts
|
|
29598
29383
|
function requestIdOf(_req) {
|
|
29599
|
-
return `req_${
|
|
29384
|
+
return `req_${nanoid31(16)}`;
|
|
29600
29385
|
}
|
|
29601
29386
|
async function actorOr401(deps, req, reply, requestId) {
|
|
29602
29387
|
const resolution = await resolvePrincipal({
|
|
@@ -30009,6 +29794,49 @@ async function apiV1ReadRoutes(app, deps) {
|
|
|
30009
29794
|
}
|
|
30010
29795
|
}
|
|
30011
29796
|
);
|
|
29797
|
+
app.get(
|
|
29798
|
+
"/projects/:projectId/deliverables",
|
|
29799
|
+
routeMeta({
|
|
29800
|
+
surface: "application",
|
|
29801
|
+
operationId: "project.deliverables",
|
|
29802
|
+
authPolicy: "application_actor",
|
|
29803
|
+
resourcePolicy: "delivery.read",
|
|
29804
|
+
stability: "preview",
|
|
29805
|
+
summary: "Tasks whose branches a developer's clone can pull"
|
|
29806
|
+
}),
|
|
29807
|
+
async (req, reply) => {
|
|
29808
|
+
const requestId = requestIdOf(req);
|
|
29809
|
+
try {
|
|
29810
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
29811
|
+
if (!actor) return reply;
|
|
29812
|
+
const { projectId } = req.params;
|
|
29813
|
+
return await read.listProjectDeliverables(actor, { projectId });
|
|
29814
|
+
} catch (err) {
|
|
29815
|
+
return sendError(reply, err, requestId);
|
|
29816
|
+
}
|
|
29817
|
+
}
|
|
29818
|
+
);
|
|
29819
|
+
app.get(
|
|
29820
|
+
"/farms",
|
|
29821
|
+
routeMeta({
|
|
29822
|
+
surface: "application",
|
|
29823
|
+
operationId: "farm.list",
|
|
29824
|
+
authPolicy: "application_actor",
|
|
29825
|
+
resourcePolicy: "account.read",
|
|
29826
|
+
stability: "preview",
|
|
29827
|
+
summary: "Farms assigned to the caller's account"
|
|
29828
|
+
}),
|
|
29829
|
+
async (req, reply) => {
|
|
29830
|
+
const requestId = requestIdOf(req);
|
|
29831
|
+
try {
|
|
29832
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
29833
|
+
if (!actor) return reply;
|
|
29834
|
+
return await read.listFarms(actor);
|
|
29835
|
+
} catch (err) {
|
|
29836
|
+
return sendError(reply, err, requestId);
|
|
29837
|
+
}
|
|
29838
|
+
}
|
|
29839
|
+
);
|
|
30012
29840
|
app.get(
|
|
30013
29841
|
"/projects/:projectId/config-proposals",
|
|
30014
29842
|
routeMeta({
|
|
@@ -30558,6 +30386,101 @@ var OBSERVER_PASS_BODY = {
|
|
|
30558
30386
|
additionalProperties: false,
|
|
30559
30387
|
properties: { note: { type: "string", maxLength: 4e3 } }
|
|
30560
30388
|
};
|
|
30389
|
+
var CREATE_PROJECT_BODY = {
|
|
30390
|
+
type: "object",
|
|
30391
|
+
additionalProperties: false,
|
|
30392
|
+
required: ["projectId", "name", "automationLevel", "repos", "contextRepo"],
|
|
30393
|
+
properties: {
|
|
30394
|
+
projectId: { type: "string", minLength: 2, maxLength: 31 },
|
|
30395
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
30396
|
+
automationLevel: { type: "string", enum: ["assisted", "full"] },
|
|
30397
|
+
trackerProvider: { type: "string", enum: ["internal", "youtrack"] },
|
|
30398
|
+
trackerProjectKey: { type: "string", minLength: 1, maxLength: 64 },
|
|
30399
|
+
repos: {
|
|
30400
|
+
type: "array",
|
|
30401
|
+
maxItems: 100,
|
|
30402
|
+
items: {
|
|
30403
|
+
...REPO_BODY,
|
|
30404
|
+
properties: {
|
|
30405
|
+
...REPO_BODY.properties,
|
|
30406
|
+
mirror: {
|
|
30407
|
+
type: "object",
|
|
30408
|
+
additionalProperties: false,
|
|
30409
|
+
required: ["host", "owner", "repoName"],
|
|
30410
|
+
properties: {
|
|
30411
|
+
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
30412
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30413
|
+
repoName: { type: "string", minLength: 1, maxLength: 200 }
|
|
30414
|
+
},
|
|
30415
|
+
description: "ADR-0041: the mirror the pipeline pushes this repository's branches to."
|
|
30416
|
+
}
|
|
30417
|
+
}
|
|
30418
|
+
}
|
|
30419
|
+
},
|
|
30420
|
+
contextRepo: {
|
|
30421
|
+
type: "object",
|
|
30422
|
+
additionalProperties: false,
|
|
30423
|
+
required: ["host", "owner", "name", "cloneUrl", "branch"],
|
|
30424
|
+
properties: {
|
|
30425
|
+
host: { type: "string", enum: ["github", "bitbucket", "gitlab", "local"] },
|
|
30426
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30427
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
30428
|
+
cloneUrl: { type: "string", minLength: 1, maxLength: 2e3 },
|
|
30429
|
+
branch: { type: "string", minLength: 1, maxLength: 250 }
|
|
30430
|
+
}
|
|
30431
|
+
},
|
|
30432
|
+
language: {
|
|
30433
|
+
type: "object",
|
|
30434
|
+
maxProperties: 20,
|
|
30435
|
+
additionalProperties: { type: "string", maxLength: 64 },
|
|
30436
|
+
description: "DEV-551: per-section language chosen at init."
|
|
30437
|
+
}
|
|
30438
|
+
}
|
|
30439
|
+
};
|
|
30440
|
+
var SEALED_HANDOFF_BODY = {
|
|
30441
|
+
type: "object",
|
|
30442
|
+
additionalProperties: false,
|
|
30443
|
+
required: ["projectId", "sealedBundle"],
|
|
30444
|
+
properties: {
|
|
30445
|
+
projectId: { type: "string", minLength: 1, maxLength: 64 },
|
|
30446
|
+
sealedBundle: { type: "string", minLength: 1 }
|
|
30447
|
+
}
|
|
30448
|
+
};
|
|
30449
|
+
var GH_RUNNERS_TEARDOWN_BODY = {
|
|
30450
|
+
type: "object",
|
|
30451
|
+
additionalProperties: false,
|
|
30452
|
+
required: ["projectId"],
|
|
30453
|
+
properties: {
|
|
30454
|
+
projectId: { type: "string", minLength: 1, maxLength: 64 },
|
|
30455
|
+
sealedBundle: { type: "string", minLength: 1 },
|
|
30456
|
+
repo: {
|
|
30457
|
+
type: "object",
|
|
30458
|
+
additionalProperties: false,
|
|
30459
|
+
required: ["owner", "repo"],
|
|
30460
|
+
properties: {
|
|
30461
|
+
owner: { type: "string", minLength: 1, maxLength: 200 },
|
|
30462
|
+
repo: { type: "string", minLength: 1, maxLength: 200 }
|
|
30463
|
+
},
|
|
30464
|
+
description: "DEV-530: the one repository being disconnected, so its binding is dropped."
|
|
30465
|
+
}
|
|
30466
|
+
}
|
|
30467
|
+
};
|
|
30468
|
+
var PROJECT_INDEX_BODY = {
|
|
30469
|
+
type: "object",
|
|
30470
|
+
additionalProperties: false,
|
|
30471
|
+
properties: {
|
|
30472
|
+
kind: { type: "string", enum: ["index", "backfill"], description: "Defaults to the ordinary index." }
|
|
30473
|
+
}
|
|
30474
|
+
};
|
|
30475
|
+
var INIT_STATUS_BODY = {
|
|
30476
|
+
type: "object",
|
|
30477
|
+
additionalProperties: false,
|
|
30478
|
+
required: ["phase"],
|
|
30479
|
+
properties: {
|
|
30480
|
+
phase: { type: "string", enum: ["context_cloning"] },
|
|
30481
|
+
error: { type: "string", minLength: 1, maxLength: 500 }
|
|
30482
|
+
}
|
|
30483
|
+
};
|
|
30561
30484
|
function responseOf(body) {
|
|
30562
30485
|
switch (body.decision) {
|
|
30563
30486
|
case "answer":
|
|
@@ -30962,6 +30885,169 @@ async function apiV1MutationRoutes(app, deps) {
|
|
|
30962
30885
|
}
|
|
30963
30886
|
}
|
|
30964
30887
|
);
|
|
30888
|
+
app.post(
|
|
30889
|
+
"/projects",
|
|
30890
|
+
routeMeta({
|
|
30891
|
+
surface: "application",
|
|
30892
|
+
operationId: "project.create",
|
|
30893
|
+
authPolicy: "application_actor",
|
|
30894
|
+
resourcePolicy: "account.manage",
|
|
30895
|
+
stability: "preview",
|
|
30896
|
+
summary: "Register a project under the caller's account",
|
|
30897
|
+
requestSchema: CREATE_PROJECT_BODY
|
|
30898
|
+
}),
|
|
30899
|
+
async (req, reply) => {
|
|
30900
|
+
const requestId = requestIdOf(req);
|
|
30901
|
+
try {
|
|
30902
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30903
|
+
if (!actor) return reply;
|
|
30904
|
+
const body = req.body;
|
|
30905
|
+
const created = await commands.createProject(actor, {
|
|
30906
|
+
project: {
|
|
30907
|
+
projectId: body.projectId,
|
|
30908
|
+
name: body.name,
|
|
30909
|
+
automationLevel: body.automationLevel,
|
|
30910
|
+
trackerProvider: body.trackerProvider ?? "youtrack",
|
|
30911
|
+
trackerProjectKey: body.trackerProjectKey ?? null,
|
|
30912
|
+
repos: body.repos,
|
|
30913
|
+
contextRepo: body.contextRepo,
|
|
30914
|
+
language: body.language ?? null
|
|
30915
|
+
},
|
|
30916
|
+
idempotencyKey: requiredIdempotencyKey(req)
|
|
30917
|
+
});
|
|
30918
|
+
return reply.status(201).header("location", `/api/v1/projects/${encodeURIComponent(created.projectId)}`).send(created);
|
|
30919
|
+
} catch (err) {
|
|
30920
|
+
return sendError(reply, err, requestId);
|
|
30921
|
+
}
|
|
30922
|
+
}
|
|
30923
|
+
);
|
|
30924
|
+
app.post(
|
|
30925
|
+
"/farms/:farmId/provision",
|
|
30926
|
+
routeMeta({
|
|
30927
|
+
surface: "application",
|
|
30928
|
+
operationId: "farm.provision",
|
|
30929
|
+
authPolicy: "application_actor",
|
|
30930
|
+
resourcePolicy: "project.manage",
|
|
30931
|
+
stability: "preview",
|
|
30932
|
+
summary: "Hand a sealed project bundle to a farm and wait for its verdict",
|
|
30933
|
+
requestSchema: SEALED_HANDOFF_BODY
|
|
30934
|
+
}),
|
|
30935
|
+
async (req, reply) => {
|
|
30936
|
+
const requestId = requestIdOf(req);
|
|
30937
|
+
try {
|
|
30938
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30939
|
+
if (!actor) return reply;
|
|
30940
|
+
const { farmId } = req.params;
|
|
30941
|
+
const { projectId, sealedBundle } = req.body;
|
|
30942
|
+
return await commands.provisionOnFarm(actor, { farmId, projectId, sealedBundle, idempotencyKey: requiredIdempotencyKey(req) });
|
|
30943
|
+
} catch (err) {
|
|
30944
|
+
return sendError(reply, err, requestId);
|
|
30945
|
+
}
|
|
30946
|
+
}
|
|
30947
|
+
);
|
|
30948
|
+
app.post(
|
|
30949
|
+
"/runners/:runnerId/gh-runners",
|
|
30950
|
+
routeMeta({
|
|
30951
|
+
surface: "application",
|
|
30952
|
+
operationId: "runner.provisionGhRunners",
|
|
30953
|
+
authPolicy: "application_actor",
|
|
30954
|
+
resourcePolicy: "project.manage",
|
|
30955
|
+
stability: "preview",
|
|
30956
|
+
summary: "Relay a sealed GitHub-runner bundle to a runner host of the caller's",
|
|
30957
|
+
requestSchema: SEALED_HANDOFF_BODY
|
|
30958
|
+
}),
|
|
30959
|
+
async (req, reply) => {
|
|
30960
|
+
const requestId = requestIdOf(req);
|
|
30961
|
+
try {
|
|
30962
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30963
|
+
if (!actor) return reply;
|
|
30964
|
+
const { runnerId } = req.params;
|
|
30965
|
+
const { projectId, sealedBundle } = req.body;
|
|
30966
|
+
return await commands.provisionGhRunners(actor, { runnerId, projectId, sealedBundle, idempotencyKey: requiredIdempotencyKey(req) });
|
|
30967
|
+
} catch (err) {
|
|
30968
|
+
return sendError(reply, err, requestId);
|
|
30969
|
+
}
|
|
30970
|
+
}
|
|
30971
|
+
);
|
|
30972
|
+
app.post(
|
|
30973
|
+
"/runners/:runnerId/gh-runners/teardown",
|
|
30974
|
+
routeMeta({
|
|
30975
|
+
surface: "application",
|
|
30976
|
+
operationId: "runner.teardownGhRunners",
|
|
30977
|
+
authPolicy: "application_actor",
|
|
30978
|
+
resourcePolicy: "project.manage",
|
|
30979
|
+
stability: "preview",
|
|
30980
|
+
summary: "Tear the project's GitHub runners down on that host",
|
|
30981
|
+
requestSchema: GH_RUNNERS_TEARDOWN_BODY
|
|
30982
|
+
}),
|
|
30983
|
+
async (req, reply) => {
|
|
30984
|
+
const requestId = requestIdOf(req);
|
|
30985
|
+
try {
|
|
30986
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
30987
|
+
if (!actor) return reply;
|
|
30988
|
+
const { runnerId } = req.params;
|
|
30989
|
+
const body = req.body;
|
|
30990
|
+
return await commands.teardownGhRunners(actor, {
|
|
30991
|
+
runnerId,
|
|
30992
|
+
projectId: body.projectId,
|
|
30993
|
+
sealedBundle: body.sealedBundle,
|
|
30994
|
+
repo: body.repo,
|
|
30995
|
+
idempotencyKey: requiredIdempotencyKey(req)
|
|
30996
|
+
});
|
|
30997
|
+
} catch (err) {
|
|
30998
|
+
return sendError(reply, err, requestId);
|
|
30999
|
+
}
|
|
31000
|
+
}
|
|
31001
|
+
);
|
|
31002
|
+
app.post(
|
|
31003
|
+
"/projects/:projectId/index",
|
|
31004
|
+
routeMeta({
|
|
31005
|
+
surface: "application",
|
|
31006
|
+
operationId: "project.index",
|
|
31007
|
+
authPolicy: "application_actor",
|
|
31008
|
+
resourcePolicy: "project.manage",
|
|
31009
|
+
stability: "preview",
|
|
31010
|
+
summary: "Queue the project's one-shot index, or its retrospective ADR backfill",
|
|
31011
|
+
requestSchema: PROJECT_INDEX_BODY
|
|
31012
|
+
}),
|
|
31013
|
+
async (req, reply) => {
|
|
31014
|
+
const requestId = requestIdOf(req);
|
|
31015
|
+
try {
|
|
31016
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31017
|
+
if (!actor) return reply;
|
|
31018
|
+
const { projectId } = req.params;
|
|
31019
|
+
const { kind } = req.body ?? {};
|
|
31020
|
+
const queued = await commands.requestProjectIndex(actor, { projectId, kind: kind ?? "index", idempotencyKey: requiredIdempotencyKey(req) });
|
|
31021
|
+
return reply.status(202).send(queued);
|
|
31022
|
+
} catch (err) {
|
|
31023
|
+
return sendError(reply, err, requestId);
|
|
31024
|
+
}
|
|
31025
|
+
}
|
|
31026
|
+
);
|
|
31027
|
+
app.post(
|
|
31028
|
+
"/projects/:projectId/init-status",
|
|
31029
|
+
routeMeta({
|
|
31030
|
+
surface: "application",
|
|
31031
|
+
operationId: "project.reportInitStatus",
|
|
31032
|
+
authPolicy: "application_actor",
|
|
31033
|
+
resourcePolicy: "project.manage",
|
|
31034
|
+
stability: "preview",
|
|
31035
|
+
summary: "Report an init step the project's client took before it connected",
|
|
31036
|
+
requestSchema: INIT_STATUS_BODY
|
|
31037
|
+
}),
|
|
31038
|
+
async (req, reply) => {
|
|
31039
|
+
const requestId = requestIdOf(req);
|
|
31040
|
+
try {
|
|
31041
|
+
const actor = await actorOr401({ db, config }, req, reply, requestId);
|
|
31042
|
+
if (!actor) return reply;
|
|
31043
|
+
const { projectId } = req.params;
|
|
31044
|
+
const { phase, error } = req.body;
|
|
31045
|
+
return await commands.reportInitStatus(actor, { projectId, phase, error });
|
|
31046
|
+
} catch (err) {
|
|
31047
|
+
return sendError(reply, err, requestId);
|
|
31048
|
+
}
|
|
31049
|
+
}
|
|
31050
|
+
);
|
|
30965
31051
|
app.post(
|
|
30966
31052
|
"/projects/:projectId/repos",
|
|
30967
31053
|
routeMeta({
|
|
@@ -31766,7 +31852,7 @@ init_regimen_config();
|
|
|
31766
31852
|
// src/projects/cred-updates.ts
|
|
31767
31853
|
init_dist();
|
|
31768
31854
|
init_ws();
|
|
31769
|
-
import { nanoid as
|
|
31855
|
+
import { nanoid as nanoid32 } from "nanoid";
|
|
31770
31856
|
var CRED_KIND_NOT_REPLACEABLE = "cred_kind_not_replaceable";
|
|
31771
31857
|
function checkCredKind(kind) {
|
|
31772
31858
|
if (isCredKind(kind)) return { ok: true, kind };
|
|
@@ -31789,7 +31875,7 @@ async function retireNonReplaceableCredUpdates(db, log) {
|
|
|
31789
31875
|
return changes;
|
|
31790
31876
|
}
|
|
31791
31877
|
async function enqueueCredUpdate(db, projectId, kind, sealed, requestedBy, log) {
|
|
31792
|
-
const id = `cred_${
|
|
31878
|
+
const id = `cred_${nanoid32(16)}`;
|
|
31793
31879
|
const now = Date.now();
|
|
31794
31880
|
await db.run(
|
|
31795
31881
|
`UPDATE cred_updates SET state = 'rejected', detail = 'superseded by a newer replacement', updated_at = ?
|
|
@@ -31908,6 +31994,7 @@ function installCredUpdateHandlers(db, log) {
|
|
|
31908
31994
|
|
|
31909
31995
|
// src/application/read-model.ts
|
|
31910
31996
|
init_ws();
|
|
31997
|
+
init_farm2();
|
|
31911
31998
|
init_queue_view();
|
|
31912
31999
|
init_state_machine2();
|
|
31913
32000
|
init_delivery_decision();
|
|
@@ -32119,7 +32206,7 @@ function configProposalRow(stored) {
|
|
|
32119
32206
|
}
|
|
32120
32207
|
|
|
32121
32208
|
// src/config-proposals/state-machine.ts
|
|
32122
|
-
import { nanoid as
|
|
32209
|
+
import { nanoid as nanoid33 } from "nanoid";
|
|
32123
32210
|
function rowToProposal(raw) {
|
|
32124
32211
|
return {
|
|
32125
32212
|
id: raw["id"],
|
|
@@ -32144,7 +32231,7 @@ var PROPOSAL_COLUMNS = `id, project_id as projectId, user_input as userInput, st
|
|
|
32144
32231
|
created_at as createdAt, updated_at as updatedAt`;
|
|
32145
32232
|
var PROPOSAL_SELECT = `SELECT ${PROPOSAL_COLUMNS} FROM config_proposals`;
|
|
32146
32233
|
async function createProposal(db, projectId, userInput) {
|
|
32147
|
-
const id = `cfp_${
|
|
32234
|
+
const id = `cfp_${nanoid33(16)}`;
|
|
32148
32235
|
const now = Date.now();
|
|
32149
32236
|
await db.run(
|
|
32150
32237
|
`INSERT INTO config_proposals
|
|
@@ -32793,7 +32880,7 @@ async function projectDetailOf(db, projectId, raw) {
|
|
|
32793
32880
|
actionErrors: getProjectActions(projectId)?.actionErrors ?? []
|
|
32794
32881
|
};
|
|
32795
32882
|
}
|
|
32796
|
-
function createReadModel(db) {
|
|
32883
|
+
function createReadModel(db, options = {}) {
|
|
32797
32884
|
const SUBMISSION_COLUMNS = `${DRAFT_COLUMNS},
|
|
32798
32885
|
(SELECT COUNT(*) FROM draft_tasks c WHERE c.parent_draft_id = draft_tasks.id) as childCount,
|
|
32799
32886
|
EXISTS (SELECT 1 FROM tasks t
|
|
@@ -33485,6 +33572,35 @@ function createReadModel(db) {
|
|
|
33485
33572
|
const p = await getPresence(db, { email: viewer.email ?? "", userId: viewer.subjectId });
|
|
33486
33573
|
return presenceRowOf(p, new Set(viewer.projectIds));
|
|
33487
33574
|
},
|
|
33575
|
+
// --- the account's fleet (DEV-927) ---
|
|
33576
|
+
async listFarms(viewer) {
|
|
33577
|
+
const isOperator = viewer.email !== null && (options.isOperator?.(viewer.email) ?? false);
|
|
33578
|
+
return await listFarms(db, { email: viewer.email ?? "", userId: viewer.subjectId, isSuperAdmin: isOperator });
|
|
33579
|
+
},
|
|
33580
|
+
async listProjectDeliverables(projectId) {
|
|
33581
|
+
const rows = await db.all(
|
|
33582
|
+
`SELECT id, tracker_issue_key AS "taskKey", title, phase, updated_at AS "updatedAt"
|
|
33583
|
+
FROM tasks
|
|
33584
|
+
WHERE project_id = ? AND (archived IS NULL OR archived = 0)
|
|
33585
|
+
AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
|
|
33586
|
+
ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
|
|
33587
|
+
updated_at DESC
|
|
33588
|
+
LIMIT 50`,
|
|
33589
|
+
projectId
|
|
33590
|
+
);
|
|
33591
|
+
const out = [];
|
|
33592
|
+
for (const r of rows) {
|
|
33593
|
+
const byRepo = await loadTaskBranchesByRepo(db, r.id);
|
|
33594
|
+
const repos = Object.entries(byRepo).map(([repoName, b]) => ({
|
|
33595
|
+
repoName,
|
|
33596
|
+
branchName: b.branchName,
|
|
33597
|
+
baseRef: b.baseRef
|
|
33598
|
+
}));
|
|
33599
|
+
if (repos.length === 0) continue;
|
|
33600
|
+
out.push({ taskKey: r.taskKey, title: r.title, phase: r.phase, updatedAt: Number(r.updatedAt), repos });
|
|
33601
|
+
}
|
|
33602
|
+
return out;
|
|
33603
|
+
},
|
|
33488
33604
|
async readNotificationSettings(subjectId) {
|
|
33489
33605
|
const user = await db.get(`SELECT 1 as hit FROM users WHERE id = ?`, subjectId);
|
|
33490
33606
|
if (!user) return null;
|
|
@@ -33573,7 +33689,7 @@ init_state_machine();
|
|
|
33573
33689
|
// src/drafts/publish.ts
|
|
33574
33690
|
init_ws();
|
|
33575
33691
|
init_state_machine();
|
|
33576
|
-
import { nanoid as
|
|
33692
|
+
import { nanoid as nanoid34 } from "nanoid";
|
|
33577
33693
|
function firstNonBlank(...vals) {
|
|
33578
33694
|
for (const v of vals) {
|
|
33579
33695
|
if (v != null && v.trim() !== "") return v;
|
|
@@ -33581,7 +33697,7 @@ function firstNonBlank(...vals) {
|
|
|
33581
33697
|
return "";
|
|
33582
33698
|
}
|
|
33583
33699
|
async function dispatchPublish(db, draft, targetStatus, log) {
|
|
33584
|
-
const publishActionId = `dpub_${
|
|
33700
|
+
const publishActionId = `dpub_${nanoid34(16)}`;
|
|
33585
33701
|
const title = firstNonBlank(draft.editedTitle, draft.generatedTitle);
|
|
33586
33702
|
const body = firstNonBlank(draft.editedBody, draft.generatedBody);
|
|
33587
33703
|
const project = await db.get(`SELECT youtrack_project_key as youtrackProjectKey FROM projects WHERE id = ?`, draft.projectId);
|
|
@@ -33727,7 +33843,7 @@ async function maybeAdvancePackage(db, parentDraftId, log) {
|
|
|
33727
33843
|
log.info({ draftId: parentDraftId }, "package published (no links) \u2192 DONE");
|
|
33728
33844
|
return;
|
|
33729
33845
|
}
|
|
33730
|
-
const linkActionId = `dpkg_${
|
|
33846
|
+
const linkActionId = `dpkg_${nanoid34(16)}`;
|
|
33731
33847
|
const action = {
|
|
33732
33848
|
type: "apply_tracker_links",
|
|
33733
33849
|
draftId: parentDraftId,
|
|
@@ -33922,7 +34038,113 @@ async function discardDraft(db, draft, log) {
|
|
|
33922
34038
|
init_dist5();
|
|
33923
34039
|
init_regimen_write();
|
|
33924
34040
|
init_ws();
|
|
33925
|
-
|
|
34041
|
+
|
|
34042
|
+
// src/farm/provision.ts
|
|
34043
|
+
init_ws();
|
|
34044
|
+
init_pool_spec();
|
|
34045
|
+
import { nanoid as nanoid35 } from "nanoid";
|
|
34046
|
+
var pending3 = /* @__PURE__ */ new Map();
|
|
34047
|
+
var RESULT_TYPES = /* @__PURE__ */ new Set(["provision_result", "delete_result", "respawn_result"]);
|
|
34048
|
+
function handleFarmResult(msg) {
|
|
34049
|
+
const type = msg["type"];
|
|
34050
|
+
if (typeof type !== "string" || !RESULT_TYPES.has(type)) return;
|
|
34051
|
+
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
34052
|
+
const p = pending3.get(requestId);
|
|
34053
|
+
if (!p) return;
|
|
34054
|
+
clearTimeout(p.timer);
|
|
34055
|
+
pending3.delete(requestId);
|
|
34056
|
+
p.resolve(msg);
|
|
34057
|
+
}
|
|
34058
|
+
function awaitResult(requestId, send, timeoutMs) {
|
|
34059
|
+
if (!send()) return Promise.reject(new Error("farm_offline"));
|
|
34060
|
+
return new Promise((resolve4, reject) => {
|
|
34061
|
+
const timer = setTimeout(() => {
|
|
34062
|
+
pending3.delete(requestId);
|
|
34063
|
+
reject(new Error("farm_timeout"));
|
|
34064
|
+
}, timeoutMs);
|
|
34065
|
+
pending3.set(requestId, { resolve: resolve4, reject, timer });
|
|
34066
|
+
});
|
|
34067
|
+
}
|
|
34068
|
+
function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
|
|
34069
|
+
const requestId = nanoid35(16);
|
|
34070
|
+
return awaitResult(
|
|
34071
|
+
requestId,
|
|
34072
|
+
() => sendToFarm(farmId, { type: "provision_project", requestId, projectId, sealedBundle }),
|
|
34073
|
+
timeoutMs
|
|
34074
|
+
);
|
|
34075
|
+
}
|
|
34076
|
+
function requestDelete(farmId, projectId, timeoutMs) {
|
|
34077
|
+
const requestId = nanoid35(16);
|
|
34078
|
+
return awaitResult(
|
|
34079
|
+
requestId,
|
|
34080
|
+
() => sendToFarm(farmId, { type: "delete_project", requestId, projectId }),
|
|
34081
|
+
timeoutMs
|
|
34082
|
+
);
|
|
34083
|
+
}
|
|
34084
|
+
function requestRespawn(farmId, projectId, timeoutMs) {
|
|
34085
|
+
const requestId = nanoid35(16);
|
|
34086
|
+
return awaitResult(
|
|
34087
|
+
requestId,
|
|
34088
|
+
() => sendToFarm(farmId, { type: "respawn_project", requestId, projectId }),
|
|
34089
|
+
timeoutMs
|
|
34090
|
+
);
|
|
34091
|
+
}
|
|
34092
|
+
async function requestFarmPoolSync(db, farmId) {
|
|
34093
|
+
const accounts = await computeFarmPoolSpec(db, farmId);
|
|
34094
|
+
sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
|
|
34095
|
+
}
|
|
34096
|
+
|
|
34097
|
+
// src/runner/provision.ts
|
|
34098
|
+
init_ws();
|
|
34099
|
+
import { nanoid as nanoid36 } from "nanoid";
|
|
34100
|
+
var pending4 = /* @__PURE__ */ new Map();
|
|
34101
|
+
var RESULT_TYPES2 = /* @__PURE__ */ new Set(["provision_gh_runners_result", "delete_gh_runners_result"]);
|
|
34102
|
+
function handleRunnerResult(msg) {
|
|
34103
|
+
const type = msg["type"];
|
|
34104
|
+
if (typeof type !== "string" || !RESULT_TYPES2.has(type)) return;
|
|
34105
|
+
const requestId = typeof msg["requestId"] === "string" ? msg["requestId"] : "";
|
|
34106
|
+
const p = pending4.get(requestId);
|
|
34107
|
+
if (!p) return;
|
|
34108
|
+
clearTimeout(p.timer);
|
|
34109
|
+
pending4.delete(requestId);
|
|
34110
|
+
p.resolve(msg);
|
|
34111
|
+
}
|
|
34112
|
+
function awaitResult2(requestId, send, timeoutMs) {
|
|
34113
|
+
if (!send()) return Promise.reject(new Error("runner_offline"));
|
|
34114
|
+
return new Promise((resolve4, reject) => {
|
|
34115
|
+
const timer = setTimeout(() => {
|
|
34116
|
+
pending4.delete(requestId);
|
|
34117
|
+
reject(new Error("runner_timeout"));
|
|
34118
|
+
}, timeoutMs);
|
|
34119
|
+
pending4.set(requestId, { resolve: resolve4, reject, timer });
|
|
34120
|
+
});
|
|
34121
|
+
}
|
|
34122
|
+
function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
34123
|
+
const requestId = nanoid36(16);
|
|
34124
|
+
return awaitResult2(
|
|
34125
|
+
requestId,
|
|
34126
|
+
() => sendToRunner(runnerId, { type: "provision_gh_runners", requestId, projectId, sealedBundle }),
|
|
34127
|
+
timeoutMs
|
|
34128
|
+
);
|
|
34129
|
+
}
|
|
34130
|
+
function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
|
|
34131
|
+
const requestId = nanoid36(16);
|
|
34132
|
+
return awaitResult2(
|
|
34133
|
+
requestId,
|
|
34134
|
+
() => sendToRunner(runnerId, {
|
|
34135
|
+
type: "delete_gh_runners",
|
|
34136
|
+
requestId,
|
|
34137
|
+
projectId,
|
|
34138
|
+
...sealedBundle ? { sealedBundle } : {}
|
|
34139
|
+
}),
|
|
34140
|
+
timeoutMs
|
|
34141
|
+
);
|
|
34142
|
+
}
|
|
34143
|
+
|
|
34144
|
+
// src/application/write-model.ts
|
|
34145
|
+
init_farm2();
|
|
34146
|
+
init_runner2();
|
|
34147
|
+
init_init_status();
|
|
33926
34148
|
|
|
33927
34149
|
// src/projects/forget.ts
|
|
33928
34150
|
init_legacy_cred_bundles();
|
|
@@ -33987,8 +34209,71 @@ async function sweepOrphanedProjectRows(db) {
|
|
|
33987
34209
|
return removed;
|
|
33988
34210
|
}
|
|
33989
34211
|
|
|
34212
|
+
// src/runner/bindings.ts
|
|
34213
|
+
function parseGhRunnerBindings(json) {
|
|
34214
|
+
if (!json) return [];
|
|
34215
|
+
try {
|
|
34216
|
+
const parsed = JSON.parse(json);
|
|
34217
|
+
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
34218
|
+
} catch {
|
|
34219
|
+
return [];
|
|
34220
|
+
}
|
|
34221
|
+
}
|
|
34222
|
+
function serialize(bindings) {
|
|
34223
|
+
return JSON.stringify({ bindings });
|
|
34224
|
+
}
|
|
34225
|
+
async function upsertGhRunnerBinding(db, projectId, binding) {
|
|
34226
|
+
await db.tx(async (tx) => {
|
|
34227
|
+
const row = await tx.get(
|
|
34228
|
+
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
34229
|
+
projectId
|
|
34230
|
+
);
|
|
34231
|
+
if (!row) return;
|
|
34232
|
+
const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
|
|
34233
|
+
(b) => !(b.owner === binding.owner && b.repo === binding.repo)
|
|
34234
|
+
);
|
|
34235
|
+
bindings.push(binding);
|
|
34236
|
+
await tx.run(
|
|
34237
|
+
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
34238
|
+
serialize(bindings),
|
|
34239
|
+
projectId
|
|
34240
|
+
);
|
|
34241
|
+
});
|
|
34242
|
+
}
|
|
34243
|
+
async function removeGhRunnerBinding(db, projectId, owner, repo) {
|
|
34244
|
+
return db.tx(async (tx) => {
|
|
34245
|
+
const row = await tx.get(
|
|
34246
|
+
`SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
|
|
34247
|
+
projectId
|
|
34248
|
+
);
|
|
34249
|
+
if (!row) return false;
|
|
34250
|
+
const bindings = parseGhRunnerBindings(row.bindingsJson);
|
|
34251
|
+
const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
|
|
34252
|
+
if (kept.length === bindings.length) return false;
|
|
34253
|
+
await tx.run(
|
|
34254
|
+
`UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
|
|
34255
|
+
serialize(kept),
|
|
34256
|
+
projectId
|
|
34257
|
+
);
|
|
34258
|
+
return true;
|
|
34259
|
+
});
|
|
34260
|
+
}
|
|
34261
|
+
async function runnerBoundProjectCount(db, runnerId) {
|
|
34262
|
+
const rows = await db.all(
|
|
34263
|
+
`SELECT gh_runner_bindings_json AS json FROM projects WHERE gh_runner_bindings_json IS NOT NULL`
|
|
34264
|
+
);
|
|
34265
|
+
let n = 0;
|
|
34266
|
+
for (const r of rows) {
|
|
34267
|
+
try {
|
|
34268
|
+
const bindings = JSON.parse(r.json).bindings ?? [];
|
|
34269
|
+
if (bindings.some((b) => b.runnerId === runnerId)) n++;
|
|
34270
|
+
} catch {
|
|
34271
|
+
}
|
|
34272
|
+
}
|
|
34273
|
+
return n;
|
|
34274
|
+
}
|
|
34275
|
+
|
|
33990
34276
|
// src/application/write-model.ts
|
|
33991
|
-
init_bindings();
|
|
33992
34277
|
init_tracking_commands();
|
|
33993
34278
|
|
|
33994
34279
|
// src/config-proposals/apply.ts
|
|
@@ -34122,10 +34407,26 @@ function conflict(reason) {
|
|
|
34122
34407
|
function stale() {
|
|
34123
34408
|
return { ok: false, refusal: "stale_revision" };
|
|
34124
34409
|
}
|
|
34410
|
+
function notFound() {
|
|
34411
|
+
return { ok: false, refusal: "not_found" };
|
|
34412
|
+
}
|
|
34413
|
+
var FARM_PROVISION_TIMEOUT_MS = 6e4;
|
|
34414
|
+
var GH_RUNNER_RELAY_TIMEOUT_MS = 12e4;
|
|
34415
|
+
function nodeUnavailable(capability, err) {
|
|
34416
|
+
const reason = err instanceof Error ? err.message : `${capability}_error`;
|
|
34417
|
+
const what = capability === "farm" ? "The farm" : "The runner host";
|
|
34418
|
+
return {
|
|
34419
|
+
ok: false,
|
|
34420
|
+
refusal: "capability_unavailable",
|
|
34421
|
+
capability,
|
|
34422
|
+
message: reason === `${capability}_timeout` ? `${what} did not answer in time. Try again once it is responsive.` : reason === `${capability}_offline` ? `${what} is offline. Start it and try again.` : `${what} could not be reached for this. Try again in a moment.`,
|
|
34423
|
+
retryable: true
|
|
34424
|
+
};
|
|
34425
|
+
}
|
|
34125
34426
|
function taskState(row) {
|
|
34126
34427
|
return { ...row, revision: String(row.updatedAt) };
|
|
34127
34428
|
}
|
|
34128
|
-
function createWriteModel(db, log) {
|
|
34429
|
+
function createWriteModel(db, log, hooks = {}) {
|
|
34129
34430
|
async function readTaskRow(projectId, key) {
|
|
34130
34431
|
const row = await db.get(
|
|
34131
34432
|
`SELECT project_id as projectId, tracker_issue_key as key, phase,
|
|
@@ -34200,6 +34501,13 @@ function createWriteModel(db, log) {
|
|
|
34200
34501
|
if (sandboxId !== null) sendToSandbox(sandboxId, { type: "pause_job", jobId: stored.sourceJobId });
|
|
34201
34502
|
await cancelActiveJobs(db, `config::${stored.id}`);
|
|
34202
34503
|
}
|
|
34504
|
+
function isOperator(actor) {
|
|
34505
|
+
return actor.email !== null && (hooks.isOperator?.(actor.email) ?? false);
|
|
34506
|
+
}
|
|
34507
|
+
async function ghRunnerHost(runnerId, actor) {
|
|
34508
|
+
const owns = isOperator(actor) ? await db.get(`SELECT id FROM runners WHERE id = ?`, runnerId) !== void 0 : await viewerOwnsRunner(db, { email: actor.email ?? "", userId: actor.subjectId }, runnerId);
|
|
34509
|
+
return owns ? ok(true) : notFound();
|
|
34510
|
+
}
|
|
34203
34511
|
return {
|
|
34204
34512
|
async readTask(projectId, taskKey) {
|
|
34205
34513
|
const row = await readTaskRow(projectId, taskKey);
|
|
@@ -34671,6 +34979,143 @@ function createWriteModel(db, log) {
|
|
|
34671
34979
|
* daemon behind it. The Context Repo remote is not touched: what the
|
|
34672
34980
|
* pipeline wrote about the work outlives the record of the project.
|
|
34673
34981
|
*/
|
|
34982
|
+
// --- registration and the account's fleet (DEV-927) ---
|
|
34983
|
+
async createProject({ project, owner }) {
|
|
34984
|
+
const trackerProvider = project.trackerProvider;
|
|
34985
|
+
const inserted = await db.tx(async (tx) => {
|
|
34986
|
+
const existing = await tx.get(`SELECT id FROM projects WHERE id = ?`, project.projectId);
|
|
34987
|
+
if (existing) return conflict("project_already_exists");
|
|
34988
|
+
const now = Date.now();
|
|
34989
|
+
await tx.run(
|
|
34990
|
+
`INSERT INTO projects (id, name, automation_level, youtrack_project_key, tracker_provider,
|
|
34991
|
+
repos_json, owner_email, owner_user_id, language_json,
|
|
34992
|
+
provisioning_status, context_repo_json, provisioned_at, provisioning_error, created_at)
|
|
34993
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'provisioned', ?, ?, NULL, ?)`,
|
|
34994
|
+
project.projectId,
|
|
34995
|
+
project.name,
|
|
34996
|
+
project.automationLevel,
|
|
34997
|
+
project.trackerProjectKey,
|
|
34998
|
+
trackerProvider,
|
|
34999
|
+
JSON.stringify(
|
|
35000
|
+
project.repos.map((r) => ({
|
|
35001
|
+
name: r.name,
|
|
35002
|
+
host: r.host,
|
|
35003
|
+
owner: r.owner,
|
|
35004
|
+
repoName: r.repoName,
|
|
35005
|
+
...r.defaultBranch ? { defaultBranch: r.defaultBranch } : {},
|
|
35006
|
+
...r.mountAs ? { mountAs: r.mountAs } : {},
|
|
35007
|
+
...r.mirror ? { mirror: { host: r.mirror.host, owner: r.mirror.owner, repoName: r.mirror.repoName } } : {}
|
|
35008
|
+
}))
|
|
35009
|
+
),
|
|
35010
|
+
owner.email,
|
|
35011
|
+
owner.subjectId,
|
|
35012
|
+
project.language === null ? null : JSON.stringify(project.language),
|
|
35013
|
+
JSON.stringify(project.contextRepo),
|
|
35014
|
+
now,
|
|
35015
|
+
now
|
|
35016
|
+
);
|
|
35017
|
+
return ok({ projectId: project.projectId });
|
|
35018
|
+
});
|
|
35019
|
+
if (!inserted.ok) return inserted;
|
|
35020
|
+
await beginInitTracking(db, project.projectId, "registered");
|
|
35021
|
+
log.info({ projectId: project.projectId, contextRepo: project.contextRepo }, "project registered, Context Repo provisioned by the client");
|
|
35022
|
+
hooks.projectRegistered?.({
|
|
35023
|
+
projectId: project.projectId,
|
|
35024
|
+
name: project.name,
|
|
35025
|
+
automationLevel: project.automationLevel,
|
|
35026
|
+
ownerEmail: owner.email,
|
|
35027
|
+
repoCount: project.repos.length
|
|
35028
|
+
});
|
|
35029
|
+
return inserted;
|
|
35030
|
+
},
|
|
35031
|
+
async provisionOnFarm({ farmId, projectId, sealedBundle, actor }) {
|
|
35032
|
+
const usable = isOperator(actor) ? await db.get(`SELECT id FROM farms WHERE id = ?`, farmId) !== void 0 : await viewerOwnsFarm(db, { email: actor.email ?? "", userId: actor.subjectId }, farmId);
|
|
35033
|
+
if (!usable) return notFound();
|
|
35034
|
+
await beginInitTracking(db, projectId, "farm_provisioning");
|
|
35035
|
+
let result;
|
|
35036
|
+
try {
|
|
35037
|
+
result = await requestProvision(farmId, projectId, sealedBundle, FARM_PROVISION_TIMEOUT_MS);
|
|
35038
|
+
} catch (err) {
|
|
35039
|
+
const reason = err instanceof Error ? err.message : "provision_failed";
|
|
35040
|
+
await recordInitError(
|
|
35041
|
+
db,
|
|
35042
|
+
projectId,
|
|
35043
|
+
reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
|
|
35044
|
+
);
|
|
35045
|
+
return nodeUnavailable("farm", err);
|
|
35046
|
+
}
|
|
35047
|
+
if (result.ok) {
|
|
35048
|
+
await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
|
|
35049
|
+
await advanceInitPhase(db, projectId, "daemon_starting");
|
|
35050
|
+
requestFarmPoolSync(db, farmId).catch(
|
|
35051
|
+
(err) => log.warn({ farmId, err }, "farm pool sync after provision failed")
|
|
35052
|
+
);
|
|
35053
|
+
} else {
|
|
35054
|
+
await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
|
|
35055
|
+
}
|
|
35056
|
+
return ok({ ok: result.ok, daemonStatus: result.daemonStatus ?? null, error: result.error ?? null });
|
|
35057
|
+
},
|
|
35058
|
+
async provisionGhRunners({ runnerId, projectId, sealedBundle, actor }) {
|
|
35059
|
+
const guard = await ghRunnerHost(runnerId, actor);
|
|
35060
|
+
if (!guard.ok) return guard;
|
|
35061
|
+
let result;
|
|
35062
|
+
try {
|
|
35063
|
+
result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, GH_RUNNER_RELAY_TIMEOUT_MS);
|
|
35064
|
+
} catch (err) {
|
|
35065
|
+
return nodeUnavailable("runner", err);
|
|
35066
|
+
}
|
|
35067
|
+
const runners = result.runners ?? [];
|
|
35068
|
+
if (result.ok) {
|
|
35069
|
+
const connectedAt = Date.now();
|
|
35070
|
+
for (const r of runners) {
|
|
35071
|
+
await upsertGhRunnerBinding(db, projectId, {
|
|
35072
|
+
owner: r.owner,
|
|
35073
|
+
repo: r.repo,
|
|
35074
|
+
runnerId,
|
|
35075
|
+
connectedAt,
|
|
35076
|
+
status: r.status,
|
|
35077
|
+
...r.error ? { error: r.error } : {},
|
|
35078
|
+
...r.labels ? { labels: r.labels } : {}
|
|
35079
|
+
});
|
|
35080
|
+
}
|
|
35081
|
+
}
|
|
35082
|
+
return ok({
|
|
35083
|
+
ok: result.ok,
|
|
35084
|
+
runners: runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error ?? null, labels: r.labels ?? [] })),
|
|
35085
|
+
error: result.error ?? null
|
|
35086
|
+
});
|
|
35087
|
+
},
|
|
35088
|
+
async teardownGhRunners({ runnerId, projectId, sealedBundle, repo, actor }) {
|
|
35089
|
+
const guard = await ghRunnerHost(runnerId, actor);
|
|
35090
|
+
if (!guard.ok) return guard;
|
|
35091
|
+
let result;
|
|
35092
|
+
try {
|
|
35093
|
+
result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle ?? void 0, GH_RUNNER_RELAY_TIMEOUT_MS);
|
|
35094
|
+
} catch (err) {
|
|
35095
|
+
return nodeUnavailable("runner", err);
|
|
35096
|
+
}
|
|
35097
|
+
if (result.ok && repo !== null) {
|
|
35098
|
+
await removeGhRunnerBinding(db, projectId, repo.owner, repo.repo);
|
|
35099
|
+
}
|
|
35100
|
+
return ok({ ok: result.ok, error: result.error ?? null });
|
|
35101
|
+
},
|
|
35102
|
+
async enqueueProjectIndex({ projectId, kind }) {
|
|
35103
|
+
const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, projectId);
|
|
35104
|
+
if (!p) return notFound();
|
|
35105
|
+
if (!p.contextRepoJson) return conflict("context_repo_not_provisioned");
|
|
35106
|
+
const jobId = kind === "backfill" ? await enqueueBackfillJob(db, projectId) : await enqueueIndexJob(db, projectId);
|
|
35107
|
+
await advanceInitPhase(db, projectId, "indexing");
|
|
35108
|
+
log.info({ projectId, jobId, kind }, kind === "backfill" ? "backfill job enqueued" : "index job enqueued");
|
|
35109
|
+
return ok({ jobId });
|
|
35110
|
+
},
|
|
35111
|
+
async reportInitStatus({ projectId, phase, error }) {
|
|
35112
|
+
const recorded = await advanceInitPhase(db, projectId, phase);
|
|
35113
|
+
if (error !== null) {
|
|
35114
|
+
const cur = await getInitStatus(db, projectId);
|
|
35115
|
+
if (cur && cur.phase === phase) await recordInitError(db, projectId, error);
|
|
35116
|
+
}
|
|
35117
|
+
return ok({ recorded });
|
|
35118
|
+
},
|
|
34674
35119
|
async deleteProject({ projectId }) {
|
|
34675
35120
|
const row = await db.get(`SELECT farm_id as farmId FROM projects WHERE id = ?`, projectId);
|
|
34676
35121
|
if (!row) return conflict("project_not_found");
|
|
@@ -36884,8 +37329,6 @@ init_settings();
|
|
|
36884
37329
|
init_users();
|
|
36885
37330
|
init_ws();
|
|
36886
37331
|
init_sandbox2();
|
|
36887
|
-
init_provision();
|
|
36888
|
-
init_bindings();
|
|
36889
37332
|
async function adminConsoleRoutes(app, deps) {
|
|
36890
37333
|
const { db, config, mailer, logFile } = deps;
|
|
36891
37334
|
app.get("/api/admin/users", async (req, reply) => {
|
|
@@ -39299,9 +39742,7 @@ init_dist();
|
|
|
39299
39742
|
init_auth();
|
|
39300
39743
|
init_project_access();
|
|
39301
39744
|
init_users();
|
|
39302
|
-
init_cli_auth();
|
|
39303
39745
|
init_lifecycle();
|
|
39304
|
-
init_queue();
|
|
39305
39746
|
init_context_outbox();
|
|
39306
39747
|
init_init_status();
|
|
39307
39748
|
init_project_config();
|
|
@@ -39510,7 +39951,6 @@ function requestTrackerFields(projectId, projectKey, timeoutMs) {
|
|
|
39510
39951
|
|
|
39511
39952
|
// src/routes/projects.ts
|
|
39512
39953
|
init_ws();
|
|
39513
|
-
init_provision();
|
|
39514
39954
|
|
|
39515
39955
|
// src/farm/move.ts
|
|
39516
39956
|
init_ws();
|
|
@@ -39594,227 +40034,10 @@ function requestGhRunnerTeardown(projectId, runner, target, timeoutMs) {
|
|
|
39594
40034
|
}
|
|
39595
40035
|
|
|
39596
40036
|
// src/routes/projects.ts
|
|
39597
|
-
init_bindings();
|
|
39598
40037
|
init_farm2();
|
|
39599
40038
|
init_runner2();
|
|
39600
|
-
var createProjectBodySchema = {
|
|
39601
|
-
type: "object",
|
|
39602
|
-
required: ["projectId", "name", "automationLevel", "repos"],
|
|
39603
|
-
properties: {
|
|
39604
|
-
projectId: { type: "string" },
|
|
39605
|
-
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
39606
|
-
automationLevel: { type: "string", enum: ["assisted", "full"] },
|
|
39607
|
-
// ADR-0034 (DEV-456): tracker provider chosen at init; absent (older CLI)
|
|
39608
|
-
// = youtrack. For `internal` youtrackProjectKey carries the key prefix.
|
|
39609
|
-
trackerProvider: { type: "string", enum: ["internal", "youtrack"] },
|
|
39610
|
-
youtrackProjectKey: { type: "string" },
|
|
39611
|
-
// DEV-200: legacy owner-email field. Still accepted so an older CLI's
|
|
39612
|
-
// request validates, but ignored — ownership always comes from the CLI
|
|
39613
|
-
// token's identity (DEV-377).
|
|
39614
|
-
ownerEmail: { type: "string" },
|
|
39615
|
-
repos: {
|
|
39616
|
-
type: "array",
|
|
39617
|
-
minItems: 0,
|
|
39618
|
-
items: {
|
|
39619
|
-
type: "object",
|
|
39620
|
-
required: ["name", "host", "owner", "repoName"],
|
|
39621
|
-
properties: {
|
|
39622
|
-
name: { type: "string", minLength: 1 },
|
|
39623
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
39624
|
-
owner: { type: "string", minLength: 1 },
|
|
39625
|
-
repoName: { type: "string", minLength: 1 },
|
|
39626
|
-
defaultBranch: { type: "string" },
|
|
39627
|
-
mountAs: { type: "string" }
|
|
39628
|
-
}
|
|
39629
|
-
}
|
|
39630
|
-
},
|
|
39631
|
-
// DEV-65 (ADR-0008): the CLI provisions the Context Repo and reports the
|
|
39632
|
-
// resolved location here. The HUB only records it — it never holds git
|
|
39633
|
-
// credentials. Absent only on the legacy HUB-provisioning path.
|
|
39634
|
-
contextRepo: {
|
|
39635
|
-
type: "object",
|
|
39636
|
-
required: ["host", "owner", "name", "cloneUrl", "branch"],
|
|
39637
|
-
properties: {
|
|
39638
|
-
// DEV-875: `local` = a bare repository on the CLI's machine, reported
|
|
39639
|
-
// with its filesystem path as `cloneUrl`. The HUB records it like any
|
|
39640
|
-
// other location — it has no git access to either kind.
|
|
39641
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab", "local"] },
|
|
39642
|
-
owner: { type: "string", minLength: 1 },
|
|
39643
|
-
name: { type: "string", minLength: 1 },
|
|
39644
|
-
cloneUrl: { type: "string", minLength: 1 },
|
|
39645
|
-
branch: { type: "string", minLength: 1 }
|
|
39646
|
-
}
|
|
39647
|
-
}
|
|
39648
|
-
}
|
|
39649
|
-
};
|
|
39650
|
-
var repoSpecSchema = {
|
|
39651
|
-
type: "object",
|
|
39652
|
-
required: ["name", "host", "owner", "repoName"],
|
|
39653
|
-
additionalProperties: false,
|
|
39654
|
-
properties: {
|
|
39655
|
-
name: { type: "string", minLength: 1, maxLength: 100 },
|
|
39656
|
-
host: { type: "string", enum: ["github", "bitbucket", "gitlab"] },
|
|
39657
|
-
owner: { type: "string", minLength: 1 },
|
|
39658
|
-
repoName: { type: "string", minLength: 1 },
|
|
39659
|
-
defaultBranch: { type: "string" },
|
|
39660
|
-
mountAs: { type: "string" }
|
|
39661
|
-
}
|
|
39662
|
-
};
|
|
39663
|
-
async function resolveRepoMutator(db, config, req, reply, projectId) {
|
|
39664
|
-
const cliAuth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39665
|
-
if (cliAuth) {
|
|
39666
|
-
if (!await canActOnProject(db, config, cliAuth, projectId)) {
|
|
39667
|
-
reply.status(403).send({ error: "forbidden" });
|
|
39668
|
-
return null;
|
|
39669
|
-
}
|
|
39670
|
-
return { email: cliAuth.email, userId: cliAuth.userId, viaCli: true };
|
|
39671
|
-
}
|
|
39672
|
-
const session = await requireProjectAccess(db, config, req, reply, projectId);
|
|
39673
|
-
if (!session) return null;
|
|
39674
|
-
return { email: session.email, userId: session.userId, viaCli: false };
|
|
39675
|
-
}
|
|
39676
|
-
async function loadRow(db, projectId) {
|
|
39677
|
-
const row = await db.get(
|
|
39678
|
-
`SELECT id, repos_json as reposJson, context_repo_json as contextRepoJson,
|
|
39679
|
-
provisioning_status as provisioningStatus
|
|
39680
|
-
FROM projects WHERE id = ?`,
|
|
39681
|
-
projectId
|
|
39682
|
-
);
|
|
39683
|
-
return {
|
|
39684
|
-
projectId: row.id,
|
|
39685
|
-
provisioningStatus: row.provisioningStatus,
|
|
39686
|
-
...row.contextRepoJson ? { contextRepo: JSON.parse(row.contextRepoJson) } : {},
|
|
39687
|
-
repos: JSON.parse(row.reposJson)
|
|
39688
|
-
};
|
|
39689
|
-
}
|
|
39690
40039
|
async function projectsRoutes(app, deps) {
|
|
39691
|
-
const { db, config
|
|
39692
|
-
app.post(
|
|
39693
|
-
"/projects",
|
|
39694
|
-
{ schema: { body: createProjectBodySchema } },
|
|
39695
|
-
async (req, reply) => {
|
|
39696
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39697
|
-
if (!auth) {
|
|
39698
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39699
|
-
}
|
|
39700
|
-
const body = req.body;
|
|
39701
|
-
if (!PROJECT_ID_RE.test(body.projectId)) {
|
|
39702
|
-
return reply.status(400).send({
|
|
39703
|
-
error: "invalid_project_id",
|
|
39704
|
-
hint: "lowercase letters, digits and dashes; starts with a letter; 2\u201331 chars"
|
|
39705
|
-
});
|
|
39706
|
-
}
|
|
39707
|
-
const existing = await db.get(`SELECT id FROM projects WHERE id = ?`, body.projectId);
|
|
39708
|
-
if (existing) {
|
|
39709
|
-
return reply.status(409).send({ error: "project_already_exists", projectId: body.projectId });
|
|
39710
|
-
}
|
|
39711
|
-
const now = Date.now();
|
|
39712
|
-
const ownerEmail = auth.email;
|
|
39713
|
-
const ownerUserId = auth.userId;
|
|
39714
|
-
const trackerProvider = body.trackerProvider === "internal" ? "internal" : "youtrack";
|
|
39715
|
-
await db.run(`INSERT INTO projects (id, name, automation_level, youtrack_project_key,
|
|
39716
|
-
tracker_provider, repos_json, owner_email, owner_user_id,
|
|
39717
|
-
provisioning_status, created_at)
|
|
39718
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`, body.projectId, body.name, body.automationLevel, body.youtrackProjectKey ?? null, trackerProvider, JSON.stringify(body.repos), ownerEmail, ownerUserId, now);
|
|
39719
|
-
if (!body.contextRepo) {
|
|
39720
|
-
await db.run(`DELETE FROM projects WHERE id = ?`, body.projectId);
|
|
39721
|
-
return reply.status(400).send({
|
|
39722
|
-
error: "context_repo_required",
|
|
39723
|
-
hint: "the CLI provisions the Context Repo and must send `contextRepo` (upgrade the CLI)"
|
|
39724
|
-
});
|
|
39725
|
-
}
|
|
39726
|
-
app.log.info(
|
|
39727
|
-
{ projectId: body.projectId, contextRepo: body.contextRepo },
|
|
39728
|
-
"project registered, Context Repo provisioned by CLI"
|
|
39729
|
-
);
|
|
39730
|
-
await db.run(`UPDATE projects
|
|
39731
|
-
SET provisioning_status = 'provisioned', context_repo_json = ?,
|
|
39732
|
-
provisioned_at = ?, provisioning_error = NULL
|
|
39733
|
-
WHERE id = ?`, JSON.stringify(body.contextRepo), Date.now(), body.projectId);
|
|
39734
|
-
await beginInitTracking(db, body.projectId, "registered");
|
|
39735
|
-
void mailer.sendProjectRegistered(config.registrationNotifyEmail, {
|
|
39736
|
-
projectName: body.name,
|
|
39737
|
-
projectId: body.projectId,
|
|
39738
|
-
automationLevel: body.automationLevel,
|
|
39739
|
-
ownerEmail: ownerEmail ?? void 0,
|
|
39740
|
-
repoCount: body.repos.length,
|
|
39741
|
-
projectUrl: `${config.baseUrl}/?project=${encodeURIComponent(body.projectId)}`
|
|
39742
|
-
}).catch(
|
|
39743
|
-
(err) => app.log.error(
|
|
39744
|
-
{ err, projectId: body.projectId },
|
|
39745
|
-
"failed to send project-registered notification"
|
|
39746
|
-
)
|
|
39747
|
-
);
|
|
39748
|
-
return reply.status(201).send(await loadRow(db, body.projectId));
|
|
39749
|
-
}
|
|
39750
|
-
);
|
|
39751
|
-
app.post(
|
|
39752
|
-
"/projects/:projectId/index",
|
|
39753
|
-
{
|
|
39754
|
-
schema: {
|
|
39755
|
-
body: {
|
|
39756
|
-
type: "object",
|
|
39757
|
-
properties: { reindex: { type: "boolean" }, backfill: { type: "boolean" } }
|
|
39758
|
-
}
|
|
39759
|
-
}
|
|
39760
|
-
},
|
|
39761
|
-
async (req, reply) => {
|
|
39762
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39763
|
-
if (!auth) {
|
|
39764
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39765
|
-
}
|
|
39766
|
-
if (!await canActOnProject(db, config, auth, req.params.projectId)) {
|
|
39767
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
39768
|
-
}
|
|
39769
|
-
const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, req.params.projectId);
|
|
39770
|
-
if (!p) return reply.status(404).send({ error: "project_not_found" });
|
|
39771
|
-
if (!p.contextRepoJson) {
|
|
39772
|
-
return reply.status(409).send({ error: "context_repo_not_provisioned" });
|
|
39773
|
-
}
|
|
39774
|
-
const backfill = req.body?.backfill === true;
|
|
39775
|
-
const jobId = backfill ? await enqueueBackfillJob(db, req.params.projectId) : await enqueueIndexJob(db, req.params.projectId);
|
|
39776
|
-
await advanceInitPhase(db, req.params.projectId, "indexing");
|
|
39777
|
-
app.log.info(
|
|
39778
|
-
{ projectId: req.params.projectId, jobId, reindex: req.body?.reindex === true, backfill },
|
|
39779
|
-
backfill ? "backfill job enqueued" : "index job enqueued"
|
|
39780
|
-
);
|
|
39781
|
-
return reply.status(202).send({ jobId });
|
|
39782
|
-
}
|
|
39783
|
-
);
|
|
39784
|
-
app.post(
|
|
39785
|
-
"/projects/:projectId/init-status",
|
|
39786
|
-
{
|
|
39787
|
-
schema: {
|
|
39788
|
-
body: {
|
|
39789
|
-
type: "object",
|
|
39790
|
-
required: ["phase"],
|
|
39791
|
-
properties: {
|
|
39792
|
-
phase: { type: "string", enum: ["context_cloning"] },
|
|
39793
|
-
error: { type: "string", minLength: 1, maxLength: 500 }
|
|
39794
|
-
}
|
|
39795
|
-
}
|
|
39796
|
-
}
|
|
39797
|
-
},
|
|
39798
|
-
async (req, reply) => {
|
|
39799
|
-
const auth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
39800
|
-
if (!auth) {
|
|
39801
|
-
return reply.status(401).send({ error: "unauthorized" });
|
|
39802
|
-
}
|
|
39803
|
-
if (!await canActOnProject(db, config, auth, req.params.projectId)) {
|
|
39804
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
39805
|
-
}
|
|
39806
|
-
const exists = await db.get(`SELECT id FROM projects WHERE id = ?`, req.params.projectId);
|
|
39807
|
-
if (!exists) return reply.status(404).send({ error: "project_not_found" });
|
|
39808
|
-
const advanced = await advanceInitPhase(db, req.params.projectId, req.body.phase);
|
|
39809
|
-
if (req.body.error) {
|
|
39810
|
-
const cur = await getInitStatus(db, req.params.projectId);
|
|
39811
|
-
if (cur && cur.phase === req.body.phase) {
|
|
39812
|
-
await recordInitError(db, req.params.projectId, req.body.error);
|
|
39813
|
-
}
|
|
39814
|
-
}
|
|
39815
|
-
return { recorded: advanced };
|
|
39816
|
-
}
|
|
39817
|
-
);
|
|
40040
|
+
const { db, config } = deps;
|
|
39818
40041
|
app.get(
|
|
39819
40042
|
"/projects/:projectId/pause",
|
|
39820
40043
|
async (req, reply) => {
|
|
@@ -40201,50 +40424,6 @@ async function projectsRoutes(app, deps) {
|
|
|
40201
40424
|
return { id, delivered };
|
|
40202
40425
|
}
|
|
40203
40426
|
);
|
|
40204
|
-
app.get(
|
|
40205
|
-
"/projects/:projectId/deliverables",
|
|
40206
|
-
async (req, reply) => {
|
|
40207
|
-
const projectId = req.params.projectId;
|
|
40208
|
-
const cliAuth = await resolveCliAuth(db, config, req.headers.authorization);
|
|
40209
|
-
if (cliAuth) {
|
|
40210
|
-
if (!await canActOnProject(db, config, cliAuth, projectId)) {
|
|
40211
|
-
return reply.status(403).send({ error: "forbidden" });
|
|
40212
|
-
}
|
|
40213
|
-
const exists = await db.get(`SELECT 1 FROM projects WHERE id = ?`, projectId);
|
|
40214
|
-
if (!exists) return reply.status(404).send({ error: "project not found" });
|
|
40215
|
-
} else if (!await requireProjectAccess(db, config, req, reply, projectId)) {
|
|
40216
|
-
return reply;
|
|
40217
|
-
}
|
|
40218
|
-
const rows = await db.all(
|
|
40219
|
-
`SELECT id, tracker_issue_key AS taskKey, title, phase, updated_at AS updatedAt
|
|
40220
|
-
FROM tasks
|
|
40221
|
-
WHERE project_id = ? AND (archived IS NULL OR archived = 0)
|
|
40222
|
-
AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
|
|
40223
|
-
ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
|
|
40224
|
-
updated_at DESC
|
|
40225
|
-
LIMIT 50`,
|
|
40226
|
-
projectId
|
|
40227
|
-
);
|
|
40228
|
-
const deliverables = [];
|
|
40229
|
-
for (const r of rows) {
|
|
40230
|
-
const byRepo = await loadTaskBranchesByRepo(db, r.id);
|
|
40231
|
-
const repos = Object.entries(byRepo).map(([repoName, b]) => ({
|
|
40232
|
-
repoName,
|
|
40233
|
-
branchName: b.branchName,
|
|
40234
|
-
baseRef: b.baseRef
|
|
40235
|
-
}));
|
|
40236
|
-
if (repos.length === 0) continue;
|
|
40237
|
-
deliverables.push({
|
|
40238
|
-
taskKey: r.taskKey,
|
|
40239
|
-
title: r.title,
|
|
40240
|
-
phase: r.phase,
|
|
40241
|
-
updatedAt: r.updatedAt,
|
|
40242
|
-
repos
|
|
40243
|
-
});
|
|
40244
|
-
}
|
|
40245
|
-
return { deliverables };
|
|
40246
|
-
}
|
|
40247
|
-
);
|
|
40248
40427
|
app.get(
|
|
40249
40428
|
"/projects/:projectId/prompts",
|
|
40250
40429
|
async (req, reply) => {
|
|
@@ -40662,88 +40841,6 @@ async function projectsRoutes(app, deps) {
|
|
|
40662
40841
|
return reply.status(202).send({ queued: true });
|
|
40663
40842
|
}
|
|
40664
40843
|
);
|
|
40665
|
-
app.post(
|
|
40666
|
-
"/projects/:projectId/repos",
|
|
40667
|
-
{ schema: { body: repoSpecSchema } },
|
|
40668
|
-
async (req, reply) => {
|
|
40669
|
-
const actor = await resolveRepoMutator(db, config, req, reply, req.params.projectId);
|
|
40670
|
-
if (!actor) return reply;
|
|
40671
|
-
const { projectId } = req.params;
|
|
40672
|
-
const repo = req.body;
|
|
40673
|
-
const mountAs = repo.mountAs ?? repo.name;
|
|
40674
|
-
const outcome = await db.tx(async (tx) => {
|
|
40675
|
-
const row = await tx.get(`SELECT repos_json as reposJson FROM projects WHERE id = ?`, projectId);
|
|
40676
|
-
if (!row) return { status: 404, error: "project_not_found" };
|
|
40677
|
-
const repos = JSON.parse(row.reposJson);
|
|
40678
|
-
if (!actor.viaCli && repos.length > 0 && !repos.some((r) => r.host === repo.host)) {
|
|
40679
|
-
return {
|
|
40680
|
-
status: 409,
|
|
40681
|
-
error: "unknown_host",
|
|
40682
|
-
hint: "add a repo on a new git host from the CLI (`codepipe repo add`), where its token is entered locally"
|
|
40683
|
-
};
|
|
40684
|
-
}
|
|
40685
|
-
if (repos.some((r) => r.name === repo.name)) return { status: 409, error: "duplicate_repo_name" };
|
|
40686
|
-
if (repos.some((r) => (r.mountAs ?? r.name) === mountAs)) {
|
|
40687
|
-
return { status: 409, error: "duplicate_mount_as" };
|
|
40688
|
-
}
|
|
40689
|
-
const added = {
|
|
40690
|
-
name: repo.name,
|
|
40691
|
-
host: repo.host,
|
|
40692
|
-
owner: repo.owner,
|
|
40693
|
-
repoName: repo.repoName,
|
|
40694
|
-
...repo.defaultBranch ? { defaultBranch: repo.defaultBranch } : {},
|
|
40695
|
-
mountAs
|
|
40696
|
-
};
|
|
40697
|
-
await tx.run(`UPDATE projects SET repos_json = ? WHERE id = ?`, JSON.stringify([...repos, added]), projectId);
|
|
40698
|
-
return { ok: true, added };
|
|
40699
|
-
});
|
|
40700
|
-
if (!("ok" in outcome)) {
|
|
40701
|
-
return reply.status(outcome.status).send({ error: outcome.error, ...outcome.hint ? { hint: outcome.hint } : {} });
|
|
40702
|
-
}
|
|
40703
|
-
if (!actor.viaCli) {
|
|
40704
|
-
const msg = { type: "provision_repo", projectId, repo: outcome.added };
|
|
40705
|
-
const delivered = sendToProjectCli(projectId, msg);
|
|
40706
|
-
app.log.info({ projectId, repo: repo.name, by: actor.email, delivered }, "repo added, provision_repo dispatched");
|
|
40707
|
-
} else {
|
|
40708
|
-
app.log.info({ projectId, repo: repo.name, by: actor.email }, "repo added by CLI");
|
|
40709
|
-
}
|
|
40710
|
-
return reply.status(201).send(await loadRow(db, projectId));
|
|
40711
|
-
}
|
|
40712
|
-
);
|
|
40713
|
-
app.delete(
|
|
40714
|
-
"/projects/:projectId/repos/:name",
|
|
40715
|
-
async (req, reply) => {
|
|
40716
|
-
const actor = await resolveRepoMutator(db, config, req, reply, req.params.projectId);
|
|
40717
|
-
if (!actor) return reply;
|
|
40718
|
-
const { projectId, name } = req.params;
|
|
40719
|
-
const purge = req.query.purge === "true" || req.query.purge === "1";
|
|
40720
|
-
const outcome = await db.tx(async (tx) => {
|
|
40721
|
-
const row = await tx.get(`SELECT repos_json as reposJson FROM projects WHERE id = ?`, projectId);
|
|
40722
|
-
if (!row) return { status: 404, error: "project_not_found" };
|
|
40723
|
-
const repos = JSON.parse(row.reposJson);
|
|
40724
|
-
const removed = repos.find((r) => r.name === name);
|
|
40725
|
-
if (!removed) return { status: 404, error: "repo_not_found" };
|
|
40726
|
-
await tx.run(
|
|
40727
|
-
`UPDATE projects SET repos_json = ? WHERE id = ?`,
|
|
40728
|
-
JSON.stringify(repos.filter((r) => r.name !== name)),
|
|
40729
|
-
projectId
|
|
40730
|
-
);
|
|
40731
|
-
return { ok: true, removed };
|
|
40732
|
-
});
|
|
40733
|
-
if (!("ok" in outcome)) return reply.status(outcome.status).send({ error: outcome.error });
|
|
40734
|
-
if (outcome.removed.host === "github") {
|
|
40735
|
-
await removeGhRunnerBinding(db, projectId, outcome.removed.owner, outcome.removed.repoName);
|
|
40736
|
-
}
|
|
40737
|
-
if (!actor.viaCli) {
|
|
40738
|
-
const msg = { type: "remove_repo", projectId, repoName: name, ...purge ? { purge: true } : {} };
|
|
40739
|
-
const delivered = sendToProjectCli(projectId, msg);
|
|
40740
|
-
app.log.info({ projectId, repo: name, by: actor.email, purge, delivered }, "repo removed, remove_repo dispatched");
|
|
40741
|
-
} else {
|
|
40742
|
-
app.log.info({ projectId, repo: name, by: actor.email }, "repo removed by CLI");
|
|
40743
|
-
}
|
|
40744
|
-
return reply.status(200).send(await loadRow(db, projectId));
|
|
40745
|
-
}
|
|
40746
|
-
);
|
|
40747
40844
|
app.get(
|
|
40748
40845
|
"/projects/:projectId/tracker-status",
|
|
40749
40846
|
async (req, reply) => {
|
|
@@ -41268,8 +41365,6 @@ async function bugReportRoutes(app, deps) {
|
|
|
41268
41365
|
|
|
41269
41366
|
// src/server.ts
|
|
41270
41367
|
init_ws();
|
|
41271
|
-
init_provision();
|
|
41272
|
-
init_provision2();
|
|
41273
41368
|
init_context_read();
|
|
41274
41369
|
var __dirname = dirname4(fileURLToPath3(import.meta.url));
|
|
41275
41370
|
var DASHBOARD_DIST = resolve3(__dirname, "../../dashboard/dist");
|
|
@@ -41532,13 +41627,28 @@ async function buildServer(config, db) {
|
|
|
41532
41627
|
await apiV1ReadRoutes(instance, {
|
|
41533
41628
|
db,
|
|
41534
41629
|
config,
|
|
41535
|
-
read: createReadOperations(createReadModel(db), authorizer)
|
|
41630
|
+
read: createReadOperations(createReadModel(db, { isOperator: (email) => isSuperUser(config, email) }), authorizer)
|
|
41536
41631
|
});
|
|
41537
41632
|
await apiV1MutationRoutes(instance, {
|
|
41538
41633
|
db,
|
|
41539
41634
|
config,
|
|
41540
41635
|
commands: createCommandOperations({
|
|
41541
|
-
write: createWriteModel(db, app.log
|
|
41636
|
+
write: createWriteModel(db, app.log, {
|
|
41637
|
+
isOperator: (email) => isSuperUser(config, email),
|
|
41638
|
+
// DEV-216: tell the operator a project was registered. Fire-and-
|
|
41639
|
+
// forget — a mail failure must not fail the registration the
|
|
41640
|
+
// client is waiting on.
|
|
41641
|
+
projectRegistered: (n) => {
|
|
41642
|
+
void mailer.sendProjectRegistered(config.registrationNotifyEmail, {
|
|
41643
|
+
projectName: n.name,
|
|
41644
|
+
projectId: n.projectId,
|
|
41645
|
+
automationLevel: n.automationLevel,
|
|
41646
|
+
ownerEmail: n.ownerEmail ?? void 0,
|
|
41647
|
+
repoCount: n.repoCount,
|
|
41648
|
+
projectUrl: `${config.baseUrl}/?project=${encodeURIComponent(n.projectId)}`
|
|
41649
|
+
}).catch((err) => app.log.error({ err, projectId: n.projectId }, "failed to send project-registered notification"));
|
|
41650
|
+
}
|
|
41651
|
+
}),
|
|
41542
41652
|
idempotency: createIdempotencyPort(db),
|
|
41543
41653
|
authorizer,
|
|
41544
41654
|
audit: createCommandAudit(db)
|