@fieldwangai/agentflow 0.1.136 → 0.1.137
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/bin/lib/prd-workflow-collaboration.mjs +172 -17
- package/bin/lib/ui-server.mjs +585 -59
- package/bin/lib/workflow-report.mjs +290 -30
- package/builtin/web-ui/dist/assets/index-CQsrSc3u.css +1 -0
- package/builtin/web-ui/dist/assets/index-DQvqqAeQ.js +590 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +3 -1
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +33 -4
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +3 -0
- package/skills/agentflow-workflow-report/SKILL.md +17 -17
- package/skills/agentflow-workflow-report/references/protocol.md +118 -45
- package/builtin/web-ui/dist/assets/index-HdswcJWY.js +0 -565
- package/builtin/web-ui/dist/assets/index-KIGufzQf.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -156,6 +156,7 @@ import {
|
|
|
156
156
|
ensurePrdWorkflowCollaboration,
|
|
157
157
|
getPrdWorkflowCollaborationById,
|
|
158
158
|
getPrdWorkflowCollaborationByShareToken,
|
|
159
|
+
getPrdWorkflowCollaborationByTapdId,
|
|
159
160
|
getPrdWorkflowCollaborationForUser,
|
|
160
161
|
ensurePrdWorkflowShareLink,
|
|
161
162
|
listPrdWorkflowCollaborationsForUser,
|
|
@@ -164,6 +165,7 @@ import {
|
|
|
164
165
|
prdWorkflowCollaborationSummary,
|
|
165
166
|
removePrdWorkflowCollaborationMember,
|
|
166
167
|
revokePrdWorkflowShareLink,
|
|
168
|
+
syncPrdWorkflowAuthority,
|
|
167
169
|
} from "./prd-workflow-collaboration.mjs";
|
|
168
170
|
import {
|
|
169
171
|
createTeam,
|
|
@@ -184,7 +186,10 @@ import {
|
|
|
184
186
|
mergeWorkflowGlobalState,
|
|
185
187
|
normalizeWorkflowReference,
|
|
186
188
|
normalizeWorkflowReport,
|
|
189
|
+
removeWorkflowGlobalStatePath,
|
|
190
|
+
workflowReportResourceKeys,
|
|
187
191
|
workflowRuntimeRevision,
|
|
192
|
+
workflowSnapshotResourceVersions,
|
|
188
193
|
} from "./workflow-report.mjs";
|
|
189
194
|
|
|
190
195
|
const MIME = {
|
|
@@ -1446,11 +1451,31 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
|
1446
1451
|
return args;
|
|
1447
1452
|
}
|
|
1448
1453
|
|
|
1449
|
-
function readBody(req) {
|
|
1454
|
+
function readBody(req, maxBytes = 5 * 1024 * 1024) {
|
|
1450
1455
|
return new Promise((resolve, reject) => {
|
|
1451
1456
|
const chunks = [];
|
|
1452
|
-
|
|
1453
|
-
|
|
1457
|
+
let total = 0;
|
|
1458
|
+
let exceeded = false;
|
|
1459
|
+
req.on("data", (chunk) => {
|
|
1460
|
+
if (exceeded) return;
|
|
1461
|
+
const value = Buffer.from(chunk);
|
|
1462
|
+
total += value.length;
|
|
1463
|
+
if (total > maxBytes) {
|
|
1464
|
+
exceeded = true;
|
|
1465
|
+
chunks.length = 0;
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
chunks.push(value);
|
|
1469
|
+
});
|
|
1470
|
+
req.on("end", () => {
|
|
1471
|
+
if (exceeded) {
|
|
1472
|
+
const error = new Error(`Request body exceeds ${maxBytes} bytes`);
|
|
1473
|
+
error.status = 413;
|
|
1474
|
+
reject(error);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
1478
|
+
});
|
|
1454
1479
|
req.on("error", reject);
|
|
1455
1480
|
});
|
|
1456
1481
|
}
|
|
@@ -3315,6 +3340,21 @@ function findWorkspaceShareUser(username) {
|
|
|
3315
3340
|
return null;
|
|
3316
3341
|
}
|
|
3317
3342
|
|
|
3343
|
+
function workflowAuthorityIdentity(value) {
|
|
3344
|
+
if (typeof value === "string" || typeof value === "number") return String(value || "").trim();
|
|
3345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
3346
|
+
return String(value.username || value.userId || value.user_id || value.nick || value.name || "").trim();
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
function workflowAuthorityIdentities(value) {
|
|
3350
|
+
const values = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
3351
|
+
return [...new Set(values.flatMap((item) => {
|
|
3352
|
+
if (typeof item === "string") return item.split(/[;,,;]/).map((entry) => entry.trim()).filter(Boolean);
|
|
3353
|
+
const identity = workflowAuthorityIdentity(item);
|
|
3354
|
+
return identity ? [identity] : [];
|
|
3355
|
+
}))];
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3318
3358
|
function workspaceCollaborationSummaryWithUsers(record, userId) {
|
|
3319
3359
|
const summary = workspaceCollaborationSummary(record, userId);
|
|
3320
3360
|
if (!summary) return null;
|
|
@@ -7780,6 +7820,7 @@ const workspaceCollaborationSequences = new Map();
|
|
|
7780
7820
|
const prdWorkflowSubscribers = new Map();
|
|
7781
7821
|
const prdWorkflowIdempotency = new Map();
|
|
7782
7822
|
const prdWorkflowActionLocks = new Map();
|
|
7823
|
+
const prdWorkflowWriteQueues = new Map();
|
|
7783
7824
|
const PRD_WORKFLOW_IDEMPOTENCY_MAX = 1000;
|
|
7784
7825
|
const PRD_WORKFLOW_RUNTIME_EVENTS_MAX = 1000;
|
|
7785
7826
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
@@ -7788,6 +7829,22 @@ const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
|
7788
7829
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
7789
7830
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
7790
7831
|
|
|
7832
|
+
async function prdWorkflowAcquireWriteLock(key) {
|
|
7833
|
+
const lockKey = String(key || "").trim();
|
|
7834
|
+
const previous = prdWorkflowWriteQueues.get(lockKey) || Promise.resolve();
|
|
7835
|
+
let releaseCurrent;
|
|
7836
|
+
const current = new Promise((resolve) => { releaseCurrent = resolve; });
|
|
7837
|
+
prdWorkflowWriteQueues.set(lockKey, current);
|
|
7838
|
+
await previous.catch(() => {});
|
|
7839
|
+
let released = false;
|
|
7840
|
+
return () => {
|
|
7841
|
+
if (released) return;
|
|
7842
|
+
released = true;
|
|
7843
|
+
releaseCurrent();
|
|
7844
|
+
if (prdWorkflowWriteQueues.get(lockKey) === current) prdWorkflowWriteQueues.delete(lockKey);
|
|
7845
|
+
};
|
|
7846
|
+
}
|
|
7847
|
+
|
|
7791
7848
|
function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capability = "read") {
|
|
7792
7849
|
const tapdId = String(params.tapdId || params.tapd_id || "").trim();
|
|
7793
7850
|
const flowId = String(params.flowId || "").trim();
|
|
@@ -7812,6 +7869,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7812
7869
|
const memberCollaboration = tapdId
|
|
7813
7870
|
? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
|
|
7814
7871
|
: null;
|
|
7872
|
+
const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
|
|
7873
|
+
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
|
|
7874
|
+
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
7875
|
+
}
|
|
7815
7876
|
const collaboration = adminOwner ? null : (linkCollaboration || memberCollaboration);
|
|
7816
7877
|
const access = adminOwner
|
|
7817
7878
|
? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
|
|
@@ -7825,7 +7886,8 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7825
7886
|
return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
|
|
7826
7887
|
}
|
|
7827
7888
|
const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
7828
|
-
const
|
|
7889
|
+
const stateOwnerId = String(adminOwner?.userId || collaboration?.stateOwnerId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
7890
|
+
const stateRoot = path.resolve(getAgentflowUserDataRoot(stateOwnerId));
|
|
7829
7891
|
let executionRoot = path.resolve(workspaceRoot);
|
|
7830
7892
|
if (flowId) {
|
|
7831
7893
|
const projectScope = resolveWorkspaceScopeRoot(workspaceRoot, {
|
|
@@ -7847,6 +7909,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7847
7909
|
executionRoot,
|
|
7848
7910
|
stateRoot,
|
|
7849
7911
|
ownerId,
|
|
7912
|
+
stateOwnerId,
|
|
7850
7913
|
collaboration,
|
|
7851
7914
|
collaborationAccess: access,
|
|
7852
7915
|
shareToken,
|
|
@@ -7863,7 +7926,7 @@ function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId =
|
|
|
7863
7926
|
const collaboration = getPrdWorkflowCollaborationByShareToken(shareToken)
|
|
7864
7927
|
|| getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
|
|
7865
7928
|
const adminOwnerId = userCtx?.isAdmin === true ? String(userCtx?.adminOwnerId || "").trim() : "";
|
|
7866
|
-
const actorScope = `user:${String(collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
|
|
7929
|
+
const actorScope = `user:${String(collaboration?.stateOwnerId || collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
|
|
7867
7930
|
return [actorScope, id].join("\t");
|
|
7868
7931
|
}
|
|
7869
7932
|
|
|
@@ -8075,6 +8138,11 @@ function prdWorkflowEventsPath(scopedRoot, tapdId) {
|
|
|
8075
8138
|
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.json`);
|
|
8076
8139
|
}
|
|
8077
8140
|
|
|
8141
|
+
function prdWorkflowEventsArchivePath(scopedRoot, tapdId) {
|
|
8142
|
+
const rootDir = scopedRoot || process.cwd();
|
|
8143
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.archive.jsonl`);
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8078
8146
|
function prdWorkflowAuditPath(scopedRoot, tapdId) {
|
|
8079
8147
|
const rootDir = scopedRoot || process.cwd();
|
|
8080
8148
|
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.audit.jsonl`);
|
|
@@ -9712,10 +9780,20 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
9712
9780
|
}
|
|
9713
9781
|
|
|
9714
9782
|
function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "", ownerId = "") {
|
|
9715
|
-
const content = String(payload.markdown || payload.content || payload.rawOutput || "")
|
|
9783
|
+
const content = String(payload.markdown || payload.content || payload.rawOutput || "");
|
|
9716
9784
|
if (!content.trim()) throw new Error("Missing review markdown");
|
|
9785
|
+
if (Buffer.byteLength(content, "utf-8") > 500000) {
|
|
9786
|
+
const error = new Error("Review markdown exceeds 500000 bytes");
|
|
9787
|
+
error.status = 413;
|
|
9788
|
+
throw error;
|
|
9789
|
+
}
|
|
9717
9790
|
const title = String(payload.title || payload.label || "PRD Workflow Review").trim().slice(0, 160) || "PRD Workflow Review";
|
|
9718
9791
|
const durability = String(payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary")).trim().toLowerCase() || "temporary";
|
|
9792
|
+
if (!["temporary", "durable"].includes(durability)) {
|
|
9793
|
+
const error = new Error("durability must be temporary or durable");
|
|
9794
|
+
error.status = 400;
|
|
9795
|
+
throw error;
|
|
9796
|
+
}
|
|
9719
9797
|
const reviewId = prdWorkflowReviewIdFromRequest(tapdId, payload, durability);
|
|
9720
9798
|
const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
|
|
9721
9799
|
const ttlDaysRaw = Number(payload.ttlDays || payload.ttl_days || (durability === "temporary" ? 7 : 0));
|
|
@@ -9935,11 +10013,13 @@ function prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId) {
|
|
|
9935
10013
|
|
|
9936
10014
|
function prdWorkflowWriteClientObservation(scopedRoot, tapdId, meta, snapshot) {
|
|
9937
10015
|
const state = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
9938
|
-
const
|
|
10016
|
+
const reportSource = String(meta.reportSource || meta.source || "legacy").trim().toLowerCase() || "legacy";
|
|
10017
|
+
const clientId = prdWorkflowSafeStateId(`${reportSource}:${meta.clientId || "anonymous"}`);
|
|
9939
10018
|
const nextClients = {
|
|
9940
10019
|
...state.clients,
|
|
9941
10020
|
[clientId]: {
|
|
9942
10021
|
clientId: String(meta.clientId || clientId),
|
|
10022
|
+
source: reportSource,
|
|
9943
10023
|
userId: String(meta.userId || ""),
|
|
9944
10024
|
observedAt: String(meta.observedAt || ""),
|
|
9945
10025
|
reportedAt: String(meta.reportedAt || new Date().toISOString()),
|
|
@@ -10172,6 +10252,7 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
|
|
|
10172
10252
|
reportedAt,
|
|
10173
10253
|
observedAt: String(payload.observedAt || payload.observed_at || rawSnapshot.observedAt || rawSnapshot.observed_at || sources.observedAt || sources.checkedAt || headerObservedAt || reportedAt),
|
|
10174
10254
|
clientId: String(payload.clientId || payload.client_id || sources.clientId || headerClientId || userCtx?.userId || "anonymous").slice(0, 160),
|
|
10255
|
+
reportSource: String(payload.reportSource || payload.report_source || payload.source || "legacy").trim().toLowerCase().slice(0, 120) || "legacy",
|
|
10175
10256
|
userId: String(userCtx?.userId || payload.userId || payload.user_id || "").slice(0, 160),
|
|
10176
10257
|
baseRevision: String(payload.baseRevision || payload.base_revision || payload.expectedRevision || payload.expected_revision || rawSnapshot.baseRevision || sources.baseRevision || "").trim(),
|
|
10177
10258
|
scope: String(payload.scope || rawSnapshot.scope || rawSnapshot.next?.scope || sources.scope || "client").trim().toLowerCase() || "client",
|
|
@@ -10218,7 +10299,7 @@ function prdWorkflowStoreClientObservation({
|
|
|
10218
10299
|
stageKey: reportMeta.stageKey,
|
|
10219
10300
|
};
|
|
10220
10301
|
const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
10221
|
-
const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
|
|
10302
|
+
const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
|
|
10222
10303
|
const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
|
|
10223
10304
|
const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
|
|
10224
10305
|
scopedRoot,
|
|
@@ -10379,6 +10460,7 @@ function prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx = {},
|
|
|
10379
10460
|
);
|
|
10380
10461
|
const clientObservations = prdWorkflowClientObservationRows(root, scopedRoot, tapdId).map((item) => ({
|
|
10381
10462
|
clientId: item.clientId,
|
|
10463
|
+
source: item.source || "legacy",
|
|
10382
10464
|
userId: item.userId || "",
|
|
10383
10465
|
phase: item.phase || "",
|
|
10384
10466
|
pointer: item.pointer || "",
|
|
@@ -10571,13 +10653,22 @@ function prdWorkflowNormalizeStoredRuntimeEvent(tapdId, event = {}) {
|
|
|
10571
10653
|
function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
|
|
10572
10654
|
try {
|
|
10573
10655
|
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
10574
|
-
|
|
10575
|
-
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
10576
|
-
const
|
|
10577
|
-
?
|
|
10578
|
-
.
|
|
10579
|
-
|
|
10656
|
+
const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
|
|
10657
|
+
const data = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf-8")) : {};
|
|
10658
|
+
const archivedEvents = fs.existsSync(archivePath)
|
|
10659
|
+
? fs.readFileSync(archivePath, "utf-8").split("\n").filter(Boolean).flatMap((line) => {
|
|
10660
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
10661
|
+
})
|
|
10580
10662
|
: [];
|
|
10663
|
+
const normalizedEvents = [...archivedEvents, ...(Array.isArray(data?.events) ? data.events : [])]
|
|
10664
|
+
.map((item) => prdWorkflowNormalizeStoredRuntimeEvent(data?.tapdId || tapdId, item))
|
|
10665
|
+
.filter((item) => item && typeof item === "object");
|
|
10666
|
+
const eventsByKey = new Map();
|
|
10667
|
+
for (const event of normalizedEvents) {
|
|
10668
|
+
const key = `${prdWorkflowRuntimeEventProducer(event)}:${prdWorkflowRuntimeEventOperation(event)}:${String(event.id || "")}`;
|
|
10669
|
+
eventsByKey.set(key, event);
|
|
10670
|
+
}
|
|
10671
|
+
const events = [...eventsByKey.values()];
|
|
10581
10672
|
return {
|
|
10582
10673
|
version: 1,
|
|
10583
10674
|
tapdId: String(data?.tapdId || tapdId || ""),
|
|
@@ -10592,11 +10683,17 @@ function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
|
|
|
10592
10683
|
function prdWorkflowWriteRuntimeEvents(scopedRoot, tapdId, events) {
|
|
10593
10684
|
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
10594
10685
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
10686
|
+
const allEvents = Array.isArray(events) ? events : [];
|
|
10687
|
+
const overflow = allEvents.slice(0, Math.max(0, allEvents.length - PRD_WORKFLOW_RUNTIME_EVENTS_MAX));
|
|
10688
|
+
const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
|
|
10689
|
+
const archiveTmp = `${archivePath}.${process.pid}.${Date.now()}.tmp`;
|
|
10690
|
+
fs.writeFileSync(archiveTmp, overflow.length ? `${overflow.map((event) => JSON.stringify(event)).join("\n")}\n` : "", "utf-8");
|
|
10691
|
+
fs.renameSync(archiveTmp, archivePath);
|
|
10595
10692
|
const data = {
|
|
10596
10693
|
version: 1,
|
|
10597
10694
|
tapdId: String(tapdId || ""),
|
|
10598
10695
|
updatedAt: new Date().toISOString(),
|
|
10599
|
-
events:
|
|
10696
|
+
events: allEvents.slice(-PRD_WORKFLOW_RUNTIME_EVENTS_MAX),
|
|
10600
10697
|
};
|
|
10601
10698
|
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
10602
10699
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
@@ -10666,6 +10763,12 @@ function prdWorkflowRuntimeEventProducer(event = {}) {
|
|
|
10666
10763
|
.slice(0, 120) || "agentflow";
|
|
10667
10764
|
}
|
|
10668
10765
|
|
|
10766
|
+
function prdWorkflowRuntimeEventOperation(event = {}) {
|
|
10767
|
+
return String(event.operation || (event.type === "review-link" ? "artifact.publish" : "report"))
|
|
10768
|
+
.trim()
|
|
10769
|
+
.toLowerCase() || "report";
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10669
10772
|
function prdWorkflowRuntimeOwnedArtifacts(values, stage = "") {
|
|
10670
10773
|
if (!Array.isArray(values)) return values;
|
|
10671
10774
|
const ownsOnlyChangedArtifact = /^(?:issue-plan|implementation|bugfix|integration):/.test(String(stage || ""));
|
|
@@ -10687,16 +10790,21 @@ function prdWorkflowRuntimeEventId(event = {}) {
|
|
|
10687
10790
|
const scope = String(event.scope || "").trim();
|
|
10688
10791
|
const platform = String(event.platform || "").trim();
|
|
10689
10792
|
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
10793
|
+
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
10794
|
+
const stableActionKey = String(event?.actionModel?.key || "").trim();
|
|
10795
|
+
if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
|
|
10796
|
+
return `stage_${prdWorkflowSafeStateId([prdWorkflowRuntimeEventProducer(event), operation, stableActionKey].join(":"))}`;
|
|
10797
|
+
}
|
|
10690
10798
|
if ((stage || action) && aggregateByStage) {
|
|
10691
10799
|
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
10692
|
-
const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10800
|
+
const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10693
10801
|
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
10694
10802
|
}
|
|
10695
10803
|
const existing = String(event.id || event.eventId || event.event_id || "").trim();
|
|
10696
10804
|
if (existing) return existing.slice(0, 160);
|
|
10697
10805
|
if (stage || action) {
|
|
10698
10806
|
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
10699
|
-
const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10807
|
+
const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10700
10808
|
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
10701
10809
|
}
|
|
10702
10810
|
return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
@@ -10866,10 +10974,12 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10866
10974
|
const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
|
|
10867
10975
|
const entryIdem = String(entry.idempotencyKey || "").trim();
|
|
10868
10976
|
const entryProducer = prdWorkflowRuntimeEventProducer(entry);
|
|
10977
|
+
const entryOperation = prdWorkflowRuntimeEventOperation(entry);
|
|
10869
10978
|
const entryDedupeKey = prdWorkflowRuntimeEventDedupeKey(entry);
|
|
10870
10979
|
const index = current.events.findIndex((item) => {
|
|
10871
10980
|
if (prdWorkflowRuntimeEventProducer(item) !== entryProducer) return false;
|
|
10872
10981
|
if (String(item?.id || "") === entry.id) return true;
|
|
10982
|
+
if (prdWorkflowRuntimeEventOperation(item) !== entryOperation) return false;
|
|
10873
10983
|
if (prdWorkflowRuntimeEventDedupeKey(item) === entryDedupeKey) return true;
|
|
10874
10984
|
if (!entryIdem) return false;
|
|
10875
10985
|
if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
|
|
@@ -10905,17 +11015,43 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10905
11015
|
incomingGlobalStatePatch,
|
|
10906
11016
|
)
|
|
10907
11017
|
: previousGlobalStatePatch;
|
|
11018
|
+
const incomingPatchPaths = Array.isArray(entry?.globalStateOwnerPaths) ? entry.globalStateOwnerPaths : [];
|
|
11019
|
+
const previousRemovePaths = Array.isArray(events[index]?.globalStateRemove || events[index]?.global_state_remove)
|
|
11020
|
+
? (events[index].globalStateRemove || events[index].global_state_remove)
|
|
11021
|
+
: [];
|
|
11022
|
+
const incomingRemovePaths = Array.isArray(entry?.globalStateRemove || entry?.global_state_remove)
|
|
11023
|
+
? (entry.globalStateRemove || entry.global_state_remove)
|
|
11024
|
+
: [];
|
|
11025
|
+
const overlapsPath = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
11026
|
+
const mergedGlobalStateRemove = [...new Set([
|
|
11027
|
+
...previousRemovePaths.filter((removedPath) => !incomingPatchPaths.some((patchPath) => overlapsPath(String(removedPath), String(patchPath)))),
|
|
11028
|
+
...incomingRemovePaths,
|
|
11029
|
+
])];
|
|
11030
|
+
let normalizedGlobalStatePatch = mergedGlobalStatePatch;
|
|
11031
|
+
for (const removedPath of incomingRemovePaths) {
|
|
11032
|
+
normalizedGlobalStatePatch = removeWorkflowGlobalStatePath(normalizedGlobalStatePatch, removedPath);
|
|
11033
|
+
}
|
|
10908
11034
|
const prevArtifact = prdWorkflowRuntimeEventArtifactSignature(events[index]);
|
|
10909
11035
|
const nextArtifact = prdWorkflowRuntimeEventArtifactSignature(entry);
|
|
10910
11036
|
artifactConflict = Boolean(prevArtifact && nextArtifact && prevArtifact !== nextArtifact &&
|
|
10911
11037
|
prdWorkflowRuntimeEventShouldConflictOnArtifact(events[index]) &&
|
|
10912
11038
|
prdWorkflowRuntimeEventShouldConflictOnArtifact(entry));
|
|
10913
|
-
const idempotencyHistory = [
|
|
10914
|
-
...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
|
|
10915
|
-
...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
|
|
11039
|
+
const idempotencyHistory = [...new Set([
|
|
10916
11040
|
...(Array.isArray(events[index].idempotencyHistory) ? events[index].idempotencyHistory : []),
|
|
11041
|
+
...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
|
|
10917
11042
|
...(Array.isArray(entry.idempotencyHistory) ? entry.idempotencyHistory : []),
|
|
10918
|
-
|
|
11043
|
+
...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
|
|
11044
|
+
])].slice(-50);
|
|
11045
|
+
const allIdempotencyFingerprints = {
|
|
11046
|
+
...(events[index].idempotencyFingerprints || {}),
|
|
11047
|
+
...(events[index].idempotencyKey && events[index].idempotencyFingerprint
|
|
11048
|
+
? { [events[index].idempotencyKey]: events[index].idempotencyFingerprint }
|
|
11049
|
+
: {}),
|
|
11050
|
+
...(entry.idempotencyFingerprints || {}),
|
|
11051
|
+
...(entry.idempotencyKey && entry.idempotencyFingerprint
|
|
11052
|
+
? { [entry.idempotencyKey]: entry.idempotencyFingerprint }
|
|
11053
|
+
: {}),
|
|
11054
|
+
};
|
|
10919
11055
|
events[index] = {
|
|
10920
11056
|
...events[index],
|
|
10921
11057
|
...entry,
|
|
@@ -10924,19 +11060,28 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10924
11060
|
outputs: prdWorkflowMergeRuntimeEventArrays(events[index].outputs, entry.outputs),
|
|
10925
11061
|
results: prdWorkflowMergeRuntimeEventArrays(events[index].results, entry.results),
|
|
10926
11062
|
actionModel: mergeWorkflowGlobalState(events[index].actionModel, entry.actionModel),
|
|
10927
|
-
globalStateRemove:
|
|
10928
|
-
|
|
10929
|
-
|
|
11063
|
+
globalStateRemove: mergedGlobalStateRemove,
|
|
11064
|
+
extensionsPatch: entry.extensionsPatch
|
|
11065
|
+
? mergeWorkflowGlobalState(events[index].extensionsPatch, entry.extensionsPatch)
|
|
11066
|
+
: events[index].extensionsPatch,
|
|
11067
|
+
globalStateOwnerPaths: prdWorkflowMergeRuntimeEventArrays(
|
|
11068
|
+
events[index].globalStateOwnerPaths,
|
|
11069
|
+
entry.globalStateOwnerPaths,
|
|
10930
11070
|
),
|
|
10931
11071
|
createdAt: events[index].createdAt || entry.createdAt,
|
|
10932
11072
|
startedAt: events[index].startedAt || entry.startedAt,
|
|
10933
|
-
idempotencyHistory
|
|
11073
|
+
idempotencyHistory,
|
|
11074
|
+
idempotencyFingerprints: Object.fromEntries(
|
|
11075
|
+
idempotencyHistory
|
|
11076
|
+
.filter((key) => allIdempotencyFingerprints[key])
|
|
11077
|
+
.map((key) => [key, allIdempotencyFingerprints[key]]),
|
|
11078
|
+
),
|
|
10934
11079
|
};
|
|
10935
11080
|
if (mergedImplementationMetadata && typeof mergedImplementationMetadata === "object" && !Array.isArray(mergedImplementationMetadata)) {
|
|
10936
11081
|
events[index].implementationMetadata = mergedImplementationMetadata;
|
|
10937
11082
|
}
|
|
10938
|
-
if (
|
|
10939
|
-
events[index].globalStatePatch =
|
|
11083
|
+
if (normalizedGlobalStatePatch && typeof normalizedGlobalStatePatch === "object" && !Array.isArray(normalizedGlobalStatePatch)) {
|
|
11084
|
+
events[index].globalStatePatch = normalizedGlobalStatePatch;
|
|
10940
11085
|
}
|
|
10941
11086
|
if (artifactConflict) {
|
|
10942
11087
|
events[index] = {
|
|
@@ -10981,43 +11126,128 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10981
11126
|
}
|
|
10982
11127
|
}
|
|
10983
11128
|
|
|
10984
|
-
function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true) {
|
|
11129
|
+
function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true, operation = "") {
|
|
10985
11130
|
const key = String(idempotencyKey || "").trim();
|
|
10986
11131
|
if (!key) return null;
|
|
10987
11132
|
const producer = String(source || "").trim().toLowerCase();
|
|
11133
|
+
const operationKey = String(operation || "").trim().toLowerCase();
|
|
10988
11134
|
const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
|
|
10989
11135
|
return [...events].reverse().find((event) => (
|
|
10990
11136
|
(!producer || prdWorkflowRuntimeEventProducer(event) === producer) &&
|
|
11137
|
+
(!operationKey || String(event?.operation || (event?.type === "review-link" ? "artifact.publish" : "report")).toLowerCase() === operationKey) &&
|
|
10991
11138
|
(String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
|
|
10992
11139
|
(!completedOnly || ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase()))
|
|
10993
11140
|
)) || null;
|
|
10994
11141
|
}
|
|
10995
11142
|
|
|
10996
11143
|
function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "") {
|
|
10997
|
-
return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source,
|
|
11144
|
+
return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source, false, "report");
|
|
11145
|
+
}
|
|
11146
|
+
|
|
11147
|
+
function prdWorkflowIdempotencyFingerprint(event = {}, idempotencyKey = "") {
|
|
11148
|
+
const key = String(idempotencyKey || "").trim();
|
|
11149
|
+
return String(
|
|
11150
|
+
event?.idempotencyFingerprints?.[key]
|
|
11151
|
+
|| (String(event?.idempotencyKey || "").trim() === key ? event?.idempotencyFingerprint : "")
|
|
11152
|
+
|| "",
|
|
11153
|
+
);
|
|
11154
|
+
}
|
|
11155
|
+
|
|
11156
|
+
function prdWorkflowResourceVersionConflicts(expectedVersions = {}, currentVersions = {}) {
|
|
11157
|
+
const conflicts = [];
|
|
11158
|
+
for (const [resourceKey, expectedVersion] of Object.entries(expectedVersions || {})) {
|
|
11159
|
+
const currentVersion = String(currentVersions?.[resourceKey] || "absent");
|
|
11160
|
+
const expected = String(expectedVersion || "absent");
|
|
11161
|
+
if (expected === currentVersion) continue;
|
|
11162
|
+
conflicts.push({ resourceKey, expectedVersion: expected, currentVersion });
|
|
11163
|
+
}
|
|
11164
|
+
return conflicts;
|
|
11165
|
+
}
|
|
11166
|
+
|
|
11167
|
+
function prdWorkflowGlobalPathOwners(snapshot = {}) {
|
|
11168
|
+
const owners = new Map();
|
|
11169
|
+
for (const event of Array.isArray(snapshot.runtimeEvents) ? snapshot.runtimeEvents : []) {
|
|
11170
|
+
const source = prdWorkflowRuntimeEventProducer(event);
|
|
11171
|
+
for (const path of Array.isArray(event?.globalStateOwnerPaths) ? event.globalStateOwnerPaths : []) {
|
|
11172
|
+
const normalized = String(path || "").trim();
|
|
11173
|
+
if (normalized) owners.set(normalized, source);
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
return owners;
|
|
11177
|
+
}
|
|
11178
|
+
|
|
11179
|
+
function prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot = {}) {
|
|
11180
|
+
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11181
|
+
const owners = prdWorkflowGlobalPathOwners(currentSnapshot);
|
|
11182
|
+
const conflicts = [];
|
|
11183
|
+
for (const path of Array.isArray(report?.event?.globalStateOwnerPaths) ? report.event.globalStateOwnerPaths : []) {
|
|
11184
|
+
const normalizedPath = String(path || "");
|
|
11185
|
+
const match = [...owners.entries()].find(([ownedPath, owner]) => (
|
|
11186
|
+
owner !== source && (
|
|
11187
|
+
ownedPath === normalizedPath ||
|
|
11188
|
+
ownedPath.startsWith(`${normalizedPath}.`) ||
|
|
11189
|
+
normalizedPath.startsWith(`${ownedPath}.`)
|
|
11190
|
+
)
|
|
11191
|
+
));
|
|
11192
|
+
if (match) conflicts.push({ path: normalizedPath, owner: match[1], source });
|
|
11193
|
+
}
|
|
11194
|
+
return conflicts;
|
|
11195
|
+
}
|
|
11196
|
+
|
|
11197
|
+
function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
|
|
11198
|
+
if (!report?.projections || !Array.isArray(report.projections.timeline)) return report;
|
|
11199
|
+
const source = prdWorkflowRuntimeEventProducer(report.event);
|
|
11200
|
+
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11201
|
+
const incoming = report.projections.timeline;
|
|
11202
|
+
const foreignCurrent = current.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
|
|
11203
|
+
const ownIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) === source);
|
|
11204
|
+
const foreignIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
|
|
11205
|
+
const foreignByKey = new Map(foreignCurrent.map((item) => [String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`), item]));
|
|
11206
|
+
for (const item of foreignIncoming) {
|
|
11207
|
+
const key = String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`);
|
|
11208
|
+
const existing = foreignByKey.get(key);
|
|
11209
|
+
const existingVersion = existing
|
|
11210
|
+
? Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [existing] } }))[0]
|
|
11211
|
+
: "";
|
|
11212
|
+
const incomingVersion = Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [item] } }))[0] || "";
|
|
11213
|
+
if (!existing || existingVersion !== incomingVersion) {
|
|
11214
|
+
return { error: `projections.timeline may not modify entries owned by source ${item?.source || "unknown"}` };
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
const timeline = [...foreignCurrent, ...ownIncoming];
|
|
11218
|
+
return {
|
|
11219
|
+
...report,
|
|
11220
|
+
projections: { ...report.projections, timeline },
|
|
11221
|
+
event: { ...report.event, projections: { ...report.event.projections, timeline } },
|
|
11222
|
+
};
|
|
10998
11223
|
}
|
|
10999
11224
|
|
|
11000
11225
|
function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
|
|
11001
11226
|
const producer = prdWorkflowRuntimeEventProducer(event);
|
|
11227
|
+
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
11002
11228
|
const stage = prdWorkflowRuntimeEventCanonicalStage(event);
|
|
11003
11229
|
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
11230
|
+
const stableActionKey = String(event?.actionModel?.key || "").trim();
|
|
11231
|
+
if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
|
|
11232
|
+
return `producer:${producer}:operation:${operation}:action:${stableActionKey}`;
|
|
11233
|
+
}
|
|
11004
11234
|
if (stage) {
|
|
11005
11235
|
const issue = event?.issueKey || event?.issue_key || event?.issue;
|
|
11006
11236
|
const platform = event?.platform;
|
|
11007
11237
|
if (aggregateByStage || issue || platform) {
|
|
11008
|
-
return ["producer", producer, "stage", event?.scope, issue, platform, stage]
|
|
11238
|
+
return ["producer", producer, "operation", operation, "stage", event?.scope, issue, platform, stage]
|
|
11009
11239
|
.map((value) => String(value || "").trim())
|
|
11010
11240
|
.join(":");
|
|
11011
11241
|
}
|
|
11012
11242
|
}
|
|
11013
11243
|
const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
|
|
11014
|
-
if (id) return `producer:${producer}:id:${id}`;
|
|
11244
|
+
if (id) return `producer:${producer}:operation:${operation}:id:${id}`;
|
|
11015
11245
|
if (stage) {
|
|
11016
|
-
return ["producer", producer, "stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
|
|
11246
|
+
return ["producer", producer, "operation", operation, "stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
|
|
11017
11247
|
.map((value) => String(value || "").trim())
|
|
11018
11248
|
.join(":");
|
|
11019
11249
|
}
|
|
11020
|
-
return `producer:${producer}:idx:${index}`;
|
|
11250
|
+
return `producer:${producer}:operation:${operation}:idx:${index}`;
|
|
11021
11251
|
}
|
|
11022
11252
|
|
|
11023
11253
|
function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
|
|
@@ -11294,7 +11524,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11294
11524
|
for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
|
|
11295
11525
|
if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
|
|
11296
11526
|
}
|
|
11297
|
-
|
|
11527
|
+
const materialized = {
|
|
11298
11528
|
...snapshot,
|
|
11299
11529
|
...prdFlowExtensionView,
|
|
11300
11530
|
workflow: globalState.workflow,
|
|
@@ -11311,6 +11541,8 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11311
11541
|
runtimeEventsUpdatedAt: runtime.updatedAt || "",
|
|
11312
11542
|
},
|
|
11313
11543
|
};
|
|
11544
|
+
materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
|
|
11545
|
+
return materialized;
|
|
11314
11546
|
}
|
|
11315
11547
|
|
|
11316
11548
|
function prdWorkflowMockSnapshot(scopedRoot, tapdId = "mock-prd") {
|
|
@@ -12714,6 +12946,95 @@ export function startUiServer({
|
|
|
12714
12946
|
json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
|
|
12715
12947
|
return;
|
|
12716
12948
|
}
|
|
12949
|
+
if (req.method === "POST" && url.pathname === "/api/workflows/access/sync") {
|
|
12950
|
+
if (!authUser?.userId) {
|
|
12951
|
+
json(res, 401, { error: "Authentication required" });
|
|
12952
|
+
return;
|
|
12953
|
+
}
|
|
12954
|
+
let payload;
|
|
12955
|
+
try {
|
|
12956
|
+
payload = JSON.parse(await readBody(req, 256 * 1024));
|
|
12957
|
+
} catch (error) {
|
|
12958
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
12959
|
+
return;
|
|
12960
|
+
}
|
|
12961
|
+
try {
|
|
12962
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
12963
|
+
if (workflow.error) {
|
|
12964
|
+
json(res, 400, { error: workflow.error });
|
|
12965
|
+
return;
|
|
12966
|
+
}
|
|
12967
|
+
if (workflow.namespace !== "tapd") {
|
|
12968
|
+
json(res, 400, { error: `Unsupported Workflow authority namespace: ${workflow.namespace}` });
|
|
12969
|
+
return;
|
|
12970
|
+
}
|
|
12971
|
+
const authorityPayload = payload?.authority && typeof payload.authority === "object" && !Array.isArray(payload.authority)
|
|
12972
|
+
? payload.authority
|
|
12973
|
+
: {};
|
|
12974
|
+
const authorityType = String(authorityPayload.type || payload.authorityType || "tapd").trim().toLowerCase();
|
|
12975
|
+
const ownerIdentity = workflowAuthorityIdentity(authorityPayload.owner ?? payload.owner);
|
|
12976
|
+
const participantIdentities = workflowAuthorityIdentities(authorityPayload.participants ?? payload.participants);
|
|
12977
|
+
if (!ownerIdentity) {
|
|
12978
|
+
json(res, 400, { error: "authority.owner is required" });
|
|
12979
|
+
return;
|
|
12980
|
+
}
|
|
12981
|
+
const ownerUser = findWorkspaceShareUser(ownerIdentity);
|
|
12982
|
+
if (!ownerUser) {
|
|
12983
|
+
json(res, 422, {
|
|
12984
|
+
error: "TAPD owner has not registered or logged in to AgentFlow",
|
|
12985
|
+
owner: ownerIdentity,
|
|
12986
|
+
});
|
|
12987
|
+
return;
|
|
12988
|
+
}
|
|
12989
|
+
const resolvedParticipants = [];
|
|
12990
|
+
const unresolvedParticipants = [];
|
|
12991
|
+
for (const identity of participantIdentities) {
|
|
12992
|
+
const user = findWorkspaceShareUser(identity);
|
|
12993
|
+
if (user) resolvedParticipants.push(user);
|
|
12994
|
+
else unresolvedParticipants.push(identity);
|
|
12995
|
+
}
|
|
12996
|
+
const result = syncPrdWorkflowAuthority({
|
|
12997
|
+
tapdId: workflow.id,
|
|
12998
|
+
userId: userCtx.userId,
|
|
12999
|
+
isAdmin: userCtx.isAdmin === true,
|
|
13000
|
+
authority: authorityType,
|
|
13001
|
+
ownerUserId: ownerUser.userId,
|
|
13002
|
+
ownerIdentity,
|
|
13003
|
+
participantUserIds: resolvedParticipants.map((user) => user.userId),
|
|
13004
|
+
participantIdentities,
|
|
13005
|
+
unresolvedParticipants,
|
|
13006
|
+
observedAt: authorityPayload.observedAt || authorityPayload.observed_at || payload.observedAt || payload.observed_at,
|
|
13007
|
+
revision: authorityPayload.revision || payload.revision,
|
|
13008
|
+
});
|
|
13009
|
+
if (result.error) {
|
|
13010
|
+
json(res, result.status || 400, { error: result.error });
|
|
13011
|
+
return;
|
|
13012
|
+
}
|
|
13013
|
+
const collaboration = prdWorkflowCollaborationSummaryWithUsers(result.record, userCtx.userId);
|
|
13014
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, "", "", workflow.id), {
|
|
13015
|
+
type: "authority.synced",
|
|
13016
|
+
tapdId: workflow.id,
|
|
13017
|
+
ownerId: result.record.ownerId,
|
|
13018
|
+
});
|
|
13019
|
+
json(res, 200, {
|
|
13020
|
+
ok: true,
|
|
13021
|
+
workflow,
|
|
13022
|
+
created: result.created === true,
|
|
13023
|
+
ownerChanged: result.ownerChanged === true,
|
|
13024
|
+
collaboration,
|
|
13025
|
+
matchedParticipants: resolvedParticipants.map((user) => ({
|
|
13026
|
+
userId: user.userId,
|
|
13027
|
+
username: user.username,
|
|
13028
|
+
role: "viewer",
|
|
13029
|
+
source: "tapd",
|
|
13030
|
+
})),
|
|
13031
|
+
unresolvedParticipants,
|
|
13032
|
+
});
|
|
13033
|
+
} catch (error) {
|
|
13034
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
13035
|
+
}
|
|
13036
|
+
return;
|
|
13037
|
+
}
|
|
12717
13038
|
if (req.method === "GET" && url.pathname === "/api/prd-workflows") {
|
|
12718
13039
|
if (!authUser?.userId) {
|
|
12719
13040
|
json(res, 401, { error: "Unauthorized" });
|
|
@@ -12737,7 +13058,7 @@ export function startUiServer({
|
|
|
12737
13058
|
records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
|
|
12738
13059
|
}
|
|
12739
13060
|
const workflows = records.map((record) => {
|
|
12740
|
-
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
|
|
13061
|
+
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.stateOwnerId || record.ownerId));
|
|
12741
13062
|
const tapdId = String(record.tapdId || "").trim();
|
|
12742
13063
|
const project = prdWorkflowReadProjectState(stateRoot, tapdId);
|
|
12743
13064
|
const latestClient = prdWorkflowLatestClientSnapshot(stateRoot, stateRoot, tapdId);
|
|
@@ -12930,7 +13251,12 @@ export function startUiServer({
|
|
|
12930
13251
|
json(res, 200, {
|
|
12931
13252
|
ok: true,
|
|
12932
13253
|
collaboration: prdWorkflowCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
12933
|
-
member: {
|
|
13254
|
+
member: {
|
|
13255
|
+
userId: targetUser.userId,
|
|
13256
|
+
username: targetUser.username,
|
|
13257
|
+
role: payload?.role === "viewer" ? "viewer" : "reporter",
|
|
13258
|
+
source: "explicit",
|
|
13259
|
+
},
|
|
12934
13260
|
});
|
|
12935
13261
|
} catch (error) {
|
|
12936
13262
|
json(res, 400, { error: (error && error.message) || String(error) });
|
|
@@ -13159,7 +13485,7 @@ export function startUiServer({
|
|
|
13159
13485
|
stageKey: reportMeta.stageKey,
|
|
13160
13486
|
};
|
|
13161
13487
|
const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
13162
|
-
const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
|
|
13488
|
+
const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
|
|
13163
13489
|
const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
|
|
13164
13490
|
const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
|
|
13165
13491
|
scopedRoot,
|
|
@@ -13834,13 +14160,14 @@ export function startUiServer({
|
|
|
13834
14160
|
}
|
|
13835
14161
|
let payload;
|
|
13836
14162
|
try {
|
|
13837
|
-
payload = JSON.parse(await readBody(req));
|
|
13838
|
-
} catch {
|
|
13839
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
14163
|
+
payload = JSON.parse(await readBody(req, 1024 * 1024));
|
|
14164
|
+
} catch (error) {
|
|
14165
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
13840
14166
|
return;
|
|
13841
14167
|
}
|
|
14168
|
+
let releaseWorkflowWriteLock = null;
|
|
13842
14169
|
try {
|
|
13843
|
-
|
|
14170
|
+
let report = normalizeWorkflowReport(payload);
|
|
13844
14171
|
if (report.error) {
|
|
13845
14172
|
json(res, 400, { error: report.error });
|
|
13846
14173
|
return;
|
|
@@ -13873,6 +14200,7 @@ export function startUiServer({
|
|
|
13873
14200
|
}
|
|
13874
14201
|
const scopedRoot = workflowScope.stateRoot;
|
|
13875
14202
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
|
|
14203
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
|
|
13876
14204
|
const currentSnapshot = prdWorkflowMaterializeSnapshot(
|
|
13877
14205
|
workflowScope.executionRoot,
|
|
13878
14206
|
scopedRoot,
|
|
@@ -13880,10 +14208,7 @@ export function startUiServer({
|
|
|
13880
14208
|
userCtx,
|
|
13881
14209
|
{ flowSource, flowId },
|
|
13882
14210
|
);
|
|
13883
|
-
const
|
|
13884
|
-
String(currentSnapshot.runtimeRevision || "").trim(),
|
|
13885
|
-
String(currentSnapshot.revision || "").trim(),
|
|
13886
|
-
].filter(Boolean));
|
|
14211
|
+
const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
|
|
13887
14212
|
if (report.idempotencyKey) {
|
|
13888
14213
|
const existing = prdWorkflowFindCompletedIdempotencyEvent(
|
|
13889
14214
|
scopedRoot,
|
|
@@ -13892,6 +14217,19 @@ export function startUiServer({
|
|
|
13892
14217
|
report.event.source,
|
|
13893
14218
|
);
|
|
13894
14219
|
if (existing) {
|
|
14220
|
+
const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, report.idempotencyKey);
|
|
14221
|
+
if (existingFingerprint && existingFingerprint !== report.event.idempotencyFingerprint) {
|
|
14222
|
+
json(res, 409, {
|
|
14223
|
+
error: "Idempotency key was already used for a different Workflow report",
|
|
14224
|
+
conflict: {
|
|
14225
|
+
type: "workflow-idempotency-conflict",
|
|
14226
|
+
idempotencyKey: report.idempotencyKey,
|
|
14227
|
+
workflow: report.workflow,
|
|
14228
|
+
},
|
|
14229
|
+
snapshot: currentSnapshot,
|
|
14230
|
+
});
|
|
14231
|
+
return;
|
|
14232
|
+
}
|
|
13895
14233
|
json(res, 200, {
|
|
13896
14234
|
ok: true,
|
|
13897
14235
|
alreadyApplied: true,
|
|
@@ -13902,19 +14240,70 @@ export function startUiServer({
|
|
|
13902
14240
|
return;
|
|
13903
14241
|
}
|
|
13904
14242
|
}
|
|
13905
|
-
|
|
14243
|
+
const ownershipConflicts = prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot);
|
|
14244
|
+
if (ownershipConflicts.length) {
|
|
14245
|
+
json(res, 409, {
|
|
14246
|
+
error: "Workflow globalState paths are owned by another report source",
|
|
14247
|
+
conflict: {
|
|
14248
|
+
type: "workflow-resource-ownership-conflict",
|
|
14249
|
+
conflicts: ownershipConflicts,
|
|
14250
|
+
workflow: report.workflow,
|
|
14251
|
+
},
|
|
14252
|
+
snapshot: currentSnapshot,
|
|
14253
|
+
});
|
|
14254
|
+
return;
|
|
14255
|
+
}
|
|
14256
|
+
const resourceKeys = workflowReportResourceKeys(report, currentSnapshot);
|
|
14257
|
+
const missingExpectedVersionKeys = Object.keys(report.expectedVersions).length
|
|
14258
|
+
? resourceKeys.filter((key) => !Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
|
|
14259
|
+
: [];
|
|
14260
|
+
if (missingExpectedVersionKeys.length) {
|
|
14261
|
+
json(res, 400, {
|
|
14262
|
+
error: "expectedVersions must include every resource key touched by this report",
|
|
14263
|
+
missingExpectedVersionKeys,
|
|
14264
|
+
resourceKeys,
|
|
14265
|
+
});
|
|
14266
|
+
return;
|
|
14267
|
+
}
|
|
14268
|
+
const expectedTouchedVersions = Object.fromEntries(
|
|
14269
|
+
resourceKeys
|
|
14270
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
|
|
14271
|
+
.map((key) => [key, report.expectedVersions[key]]),
|
|
14272
|
+
);
|
|
14273
|
+
const resourceConflicts = prdWorkflowResourceVersionConflicts(
|
|
14274
|
+
expectedTouchedVersions,
|
|
14275
|
+
currentSnapshot.resourceVersions || {},
|
|
14276
|
+
);
|
|
14277
|
+
if (resourceConflicts.length) {
|
|
14278
|
+
json(res, 409, {
|
|
14279
|
+
error: "Workflow resources changed; refresh the conflicting keys before reporting",
|
|
14280
|
+
conflict: {
|
|
14281
|
+
type: "workflow-resource-conflict",
|
|
14282
|
+
conflicts: resourceConflicts,
|
|
14283
|
+
workflow: report.workflow,
|
|
14284
|
+
},
|
|
14285
|
+
snapshot: currentSnapshot,
|
|
14286
|
+
});
|
|
14287
|
+
return;
|
|
14288
|
+
}
|
|
14289
|
+
if (!Object.keys(report.expectedVersions).length && report.expectedRevision && currentRuntimeRevision && report.expectedRevision !== currentRuntimeRevision) {
|
|
13906
14290
|
json(res, 409, {
|
|
13907
14291
|
error: "Workflow state changed; refresh before reporting",
|
|
13908
14292
|
conflict: {
|
|
13909
14293
|
type: "workflow-revision-conflict",
|
|
13910
14294
|
expectedRevision: report.expectedRevision,
|
|
13911
|
-
currentRevision:
|
|
14295
|
+
currentRevision: currentRuntimeRevision,
|
|
13912
14296
|
workflow: report.workflow,
|
|
13913
14297
|
},
|
|
13914
14298
|
snapshot: currentSnapshot,
|
|
13915
14299
|
});
|
|
13916
14300
|
return;
|
|
13917
14301
|
}
|
|
14302
|
+
report = prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
14303
|
+
if (report.error) {
|
|
14304
|
+
json(res, 400, { error: report.error });
|
|
14305
|
+
return;
|
|
14306
|
+
}
|
|
13918
14307
|
let observation = null;
|
|
13919
14308
|
if (report.observation) {
|
|
13920
14309
|
const observationPayload = {
|
|
@@ -13923,6 +14312,7 @@ export function startUiServer({
|
|
|
13923
14312
|
clientId: report.observation.clientId || payload.clientId || payload.source || "workflow-reporter",
|
|
13924
14313
|
observedAt: report.observation.observedAt || payload.observedAt || "",
|
|
13925
14314
|
scope: report.observation.scope || payload.scope || "client",
|
|
14315
|
+
reportSource: report.event.source,
|
|
13926
14316
|
};
|
|
13927
14317
|
observation = prdWorkflowStoreClientObservation({
|
|
13928
14318
|
scopedRoot,
|
|
@@ -13962,6 +14352,7 @@ export function startUiServer({
|
|
|
13962
14352
|
json(res, 200, {
|
|
13963
14353
|
ok: true,
|
|
13964
14354
|
report,
|
|
14355
|
+
resourceKeys,
|
|
13965
14356
|
event,
|
|
13966
14357
|
observation: observation ? {
|
|
13967
14358
|
accepted: true,
|
|
@@ -13973,6 +14364,8 @@ export function startUiServer({
|
|
|
13973
14364
|
});
|
|
13974
14365
|
} catch (e) {
|
|
13975
14366
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
14367
|
+
} finally {
|
|
14368
|
+
releaseWorkflowWriteLock?.();
|
|
13976
14369
|
}
|
|
13977
14370
|
return;
|
|
13978
14371
|
}
|
|
@@ -14056,11 +14449,12 @@ export function startUiServer({
|
|
|
14056
14449
|
}
|
|
14057
14450
|
let payload;
|
|
14058
14451
|
try {
|
|
14059
|
-
payload = JSON.parse(await readBody(req));
|
|
14060
|
-
} catch {
|
|
14061
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
14452
|
+
payload = JSON.parse(await readBody(req, 600000));
|
|
14453
|
+
} catch (error) {
|
|
14454
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
14062
14455
|
return;
|
|
14063
14456
|
}
|
|
14457
|
+
let releaseWorkflowWriteLock = null;
|
|
14064
14458
|
try {
|
|
14065
14459
|
const workflow = normalizeWorkflowReference(payload);
|
|
14066
14460
|
if (workflow.error) {
|
|
@@ -14088,14 +14482,85 @@ export function startUiServer({
|
|
|
14088
14482
|
}
|
|
14089
14483
|
const scopedRoot = workflowScope.stateRoot;
|
|
14090
14484
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
|
|
14091
|
-
const producer = String(payload.source || "
|
|
14485
|
+
const producer = String(payload.source || (legacyReviewEndpoint ? "prd-flow" : "")).trim().toLowerCase();
|
|
14486
|
+
if (!producer) {
|
|
14487
|
+
json(res, 400, { error: "Workflow artifact publish requires source" });
|
|
14488
|
+
return;
|
|
14489
|
+
}
|
|
14092
14490
|
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(producer)) {
|
|
14093
14491
|
json(res, 400, { error: "Invalid workflow report source" });
|
|
14094
14492
|
return;
|
|
14095
14493
|
}
|
|
14494
|
+
const fieldLimits = [
|
|
14495
|
+
[payload.title || payload.label, 160, "title"],
|
|
14496
|
+
[payload.stage || payload.stageKey || payload.stage_key, 240, "stage"],
|
|
14497
|
+
[payload.issueKey || payload.issue_key || payload.issue, 240, "issueKey"],
|
|
14498
|
+
[payload.platform, 80, "platform"],
|
|
14499
|
+
[payload.artifactLabel, 500, "artifactLabel"],
|
|
14500
|
+
[payload.reviewId || payload.review_id, 500, "reviewId"],
|
|
14501
|
+
];
|
|
14502
|
+
const oversizedField = fieldLimits.find(([value, max]) => String(value || "").trim().length > max);
|
|
14503
|
+
if (oversizedField) {
|
|
14504
|
+
json(res, 400, { error: `${oversizedField[2]} exceeds ${oversizedField[1]} characters` });
|
|
14505
|
+
return;
|
|
14506
|
+
}
|
|
14507
|
+
const markdown = String(payload.markdown || payload.content || payload.rawOutput || "");
|
|
14508
|
+
if (!markdown.trim()) {
|
|
14509
|
+
json(res, 400, { error: "Missing review markdown" });
|
|
14510
|
+
return;
|
|
14511
|
+
}
|
|
14512
|
+
if (Buffer.byteLength(markdown, "utf-8") > 500000) {
|
|
14513
|
+
json(res, 413, { error: "Review markdown exceeds 500000 bytes" });
|
|
14514
|
+
return;
|
|
14515
|
+
}
|
|
14516
|
+
const requestedDurability = String(
|
|
14517
|
+
payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary"),
|
|
14518
|
+
).trim().toLowerCase() || "temporary";
|
|
14519
|
+
if (!["temporary", "durable"].includes(requestedDurability)) {
|
|
14520
|
+
json(res, 400, { error: "durability must be temporary or durable" });
|
|
14521
|
+
return;
|
|
14522
|
+
}
|
|
14523
|
+
const ttlInput = payload.ttlDays ?? payload.ttl_days;
|
|
14524
|
+
if (requestedDurability === "temporary" && ttlInput != null) {
|
|
14525
|
+
const ttlDays = Number(ttlInput);
|
|
14526
|
+
if (!Number.isInteger(ttlDays) || ttlDays < 1 || ttlDays > 30) {
|
|
14527
|
+
json(res, 400, { error: "ttlDays must be an integer between 1 and 30" });
|
|
14528
|
+
return;
|
|
14529
|
+
}
|
|
14530
|
+
}
|
|
14531
|
+
const explicitExpiresAt = String(payload.expiresAt || payload.expires_at || "").trim();
|
|
14532
|
+
if (explicitExpiresAt && (!Number.isFinite(Date.parse(explicitExpiresAt)) || Date.parse(explicitExpiresAt) <= Date.now())) {
|
|
14533
|
+
json(res, 400, { error: "expiresAt must be a valid future date" });
|
|
14534
|
+
return;
|
|
14535
|
+
}
|
|
14096
14536
|
const idempotencyKey = String(
|
|
14097
14537
|
payload.idempotencyKey || payload.idempotency_key || "",
|
|
14098
14538
|
).trim();
|
|
14539
|
+
if (idempotencyKey.length > 500) {
|
|
14540
|
+
json(res, 400, { error: "idempotencyKey exceeds 500 characters" });
|
|
14541
|
+
return;
|
|
14542
|
+
}
|
|
14543
|
+
if (String(payload.artifactKey || payload.artifact_key || "").trim().length > 500) {
|
|
14544
|
+
json(res, 400, { error: "artifactKey exceeds 500 characters" });
|
|
14545
|
+
return;
|
|
14546
|
+
}
|
|
14547
|
+
const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
|
|
14548
|
+
const idempotencyFingerprint = prdWorkflowRevisionHash({
|
|
14549
|
+
operation: "artifact.publish",
|
|
14550
|
+
workflow,
|
|
14551
|
+
producer,
|
|
14552
|
+
title: String(payload.title || payload.label || "").trim(),
|
|
14553
|
+
markdown,
|
|
14554
|
+
stage: String(payload.stage || payload.stageKey || payload.stage_key || "").trim(),
|
|
14555
|
+
issueKey: String(payload.issueKey || payload.issue_key || payload.issue || "").trim(),
|
|
14556
|
+
platform: String(payload.platform || "").trim(),
|
|
14557
|
+
artifactKey,
|
|
14558
|
+
artifactLabel: String(payload.artifactLabel || "").trim(),
|
|
14559
|
+
durability: requestedDurability,
|
|
14560
|
+
ttlDays: ttlInput ?? null,
|
|
14561
|
+
expiresAt: explicitExpiresAt,
|
|
14562
|
+
});
|
|
14563
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
|
|
14099
14564
|
const currentSnapshot = prdWorkflowMaterializeSnapshot(
|
|
14100
14565
|
workflowScope.executionRoot,
|
|
14101
14566
|
scopedRoot,
|
|
@@ -14104,10 +14569,11 @@ export function startUiServer({
|
|
|
14104
14569
|
{ flowSource, flowId },
|
|
14105
14570
|
);
|
|
14106
14571
|
const expectedRevision = String(payload.expectedRevision || payload.expected_revision || "").trim();
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14572
|
+
if (expectedRevision.length > 500) {
|
|
14573
|
+
json(res, 400, { error: "expectedRevision exceeds 500 characters" });
|
|
14574
|
+
return;
|
|
14575
|
+
}
|
|
14576
|
+
const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
|
|
14111
14577
|
if (idempotencyKey) {
|
|
14112
14578
|
const existing = prdWorkflowFindIdempotencyEvent(
|
|
14113
14579
|
scopedRoot,
|
|
@@ -14115,8 +14581,18 @@ export function startUiServer({
|
|
|
14115
14581
|
idempotencyKey,
|
|
14116
14582
|
producer,
|
|
14117
14583
|
false,
|
|
14584
|
+
"artifact.publish",
|
|
14118
14585
|
);
|
|
14119
14586
|
if (existing) {
|
|
14587
|
+
const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, idempotencyKey);
|
|
14588
|
+
if (existingFingerprint && existingFingerprint !== idempotencyFingerprint) {
|
|
14589
|
+
json(res, 409, {
|
|
14590
|
+
error: "Idempotency key was already used for different Artifact content",
|
|
14591
|
+
conflict: { type: "workflow-idempotency-conflict", idempotencyKey, workflow },
|
|
14592
|
+
snapshot: currentSnapshot,
|
|
14593
|
+
});
|
|
14594
|
+
return;
|
|
14595
|
+
}
|
|
14120
14596
|
const artifact = Array.isArray(existing.artifacts) ? existing.artifacts[0] : null;
|
|
14121
14597
|
json(res, 200, {
|
|
14122
14598
|
ok: true,
|
|
@@ -14137,13 +14613,57 @@ export function startUiServer({
|
|
|
14137
14613
|
return;
|
|
14138
14614
|
}
|
|
14139
14615
|
}
|
|
14140
|
-
|
|
14616
|
+
const resourceKey = `artifact:${producer}:${artifactKey}`;
|
|
14617
|
+
const hasExpectedVersionsField = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
|
|
14618
|
+
|| Object.prototype.hasOwnProperty.call(payload, "expected_versions");
|
|
14619
|
+
const rawExpectedVersionsInput = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
|
|
14620
|
+
? payload.expectedVersions
|
|
14621
|
+
: payload.expected_versions;
|
|
14622
|
+
if (hasExpectedVersionsField && (!rawExpectedVersionsInput || typeof rawExpectedVersionsInput !== "object" || Array.isArray(rawExpectedVersionsInput))) {
|
|
14623
|
+
json(res, 400, { error: "expectedVersions must be an object" });
|
|
14624
|
+
return;
|
|
14625
|
+
}
|
|
14626
|
+
const rawExpectedVersions = hasExpectedVersionsField ? rawExpectedVersionsInput : {};
|
|
14627
|
+
const invalidExpectedVersionEntry = Object.entries(rawExpectedVersions).find(([key, value]) => (
|
|
14628
|
+
!String(key || "").trim() || String(key).length > 800 || /[\0\r\n]/.test(String(key)) ||
|
|
14629
|
+
String(value == null || value === "" ? "absent" : value).trim().length > 160
|
|
14630
|
+
));
|
|
14631
|
+
if (invalidExpectedVersionEntry) {
|
|
14632
|
+
json(res, 400, { error: "expectedVersions contains an invalid resource key or version" });
|
|
14633
|
+
return;
|
|
14634
|
+
}
|
|
14635
|
+
if (Object.keys(rawExpectedVersions).length && !Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)) {
|
|
14636
|
+
json(res, 400, {
|
|
14637
|
+
error: "expectedVersions must include the Artifact resource key touched by this publish",
|
|
14638
|
+
missingExpectedVersionKeys: [resourceKey],
|
|
14639
|
+
resourceKeys: [resourceKey],
|
|
14640
|
+
});
|
|
14641
|
+
return;
|
|
14642
|
+
}
|
|
14643
|
+
const expectedArtifactVersion = Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)
|
|
14644
|
+
? String(rawExpectedVersions[resourceKey] || "absent")
|
|
14645
|
+
: null;
|
|
14646
|
+
const resourceConflicts = expectedArtifactVersion == null
|
|
14647
|
+
? []
|
|
14648
|
+
: prdWorkflowResourceVersionConflicts(
|
|
14649
|
+
{ [resourceKey]: expectedArtifactVersion },
|
|
14650
|
+
currentSnapshot.resourceVersions || {},
|
|
14651
|
+
);
|
|
14652
|
+
if (resourceConflicts.length) {
|
|
14653
|
+
json(res, 409, {
|
|
14654
|
+
error: "Workflow artifact changed; refresh before publishing",
|
|
14655
|
+
conflict: { type: "workflow-resource-conflict", conflicts: resourceConflicts, workflow },
|
|
14656
|
+
snapshot: currentSnapshot,
|
|
14657
|
+
});
|
|
14658
|
+
return;
|
|
14659
|
+
}
|
|
14660
|
+
if (!Object.keys(rawExpectedVersions).length && expectedRevision && currentRuntimeRevision && expectedRevision !== currentRuntimeRevision) {
|
|
14141
14661
|
json(res, 409, {
|
|
14142
14662
|
error: "Workflow state changed; refresh before publishing",
|
|
14143
14663
|
conflict: {
|
|
14144
14664
|
type: "workflow-revision-conflict",
|
|
14145
14665
|
expectedRevision,
|
|
14146
|
-
currentRevision:
|
|
14666
|
+
currentRevision: currentRuntimeRevision,
|
|
14147
14667
|
workflow,
|
|
14148
14668
|
},
|
|
14149
14669
|
snapshot: currentSnapshot,
|
|
@@ -14171,7 +14691,6 @@ export function startUiServer({
|
|
|
14171
14691
|
const shortUrl = shortLink?.shortUrl || "";
|
|
14172
14692
|
const displayUrl = shortUrl || reviewUrl;
|
|
14173
14693
|
const durability = review.durability || "temporary";
|
|
14174
|
-
const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
|
|
14175
14694
|
const reviewStageKey = prdWorkflowRuntimeEventCanonicalStage(payload)
|
|
14176
14695
|
|| payload.stageKey
|
|
14177
14696
|
|| payload.stage_key
|
|
@@ -14206,6 +14725,7 @@ export function startUiServer({
|
|
|
14206
14725
|
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
14207
14726
|
id: `review-link:${artifactKey}`,
|
|
14208
14727
|
type: "review-link",
|
|
14728
|
+
operation: "artifact.publish",
|
|
14209
14729
|
source: producer,
|
|
14210
14730
|
auxiliary: true,
|
|
14211
14731
|
aggregateByStage: false,
|
|
@@ -14224,6 +14744,8 @@ export function startUiServer({
|
|
|
14224
14744
|
...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
|
|
14225
14745
|
...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
|
|
14226
14746
|
idempotencyKey,
|
|
14747
|
+
idempotencyFingerprint,
|
|
14748
|
+
idempotencyFingerprints: idempotencyKey ? { [idempotencyKey]: idempotencyFingerprint } : {},
|
|
14227
14749
|
durability,
|
|
14228
14750
|
sourceArtifact: reviewSource,
|
|
14229
14751
|
expiresAt: review.expiresAt || "",
|
|
@@ -14263,6 +14785,7 @@ export function startUiServer({
|
|
|
14263
14785
|
ok: true,
|
|
14264
14786
|
workflow,
|
|
14265
14787
|
artifact,
|
|
14788
|
+
resourceKeys: [resourceKey],
|
|
14266
14789
|
review: {
|
|
14267
14790
|
...review,
|
|
14268
14791
|
url: reviewUrl,
|
|
@@ -14279,7 +14802,10 @@ export function startUiServer({
|
|
|
14279
14802
|
} : {}),
|
|
14280
14803
|
});
|
|
14281
14804
|
} catch (e) {
|
|
14282
|
-
|
|
14805
|
+
const status = Number(e?.status);
|
|
14806
|
+
json(res, status >= 400 && status < 500 ? status : 500, { error: (e && e.message) || String(e) });
|
|
14807
|
+
} finally {
|
|
14808
|
+
releaseWorkflowWriteLock?.();
|
|
14283
14809
|
}
|
|
14284
14810
|
return;
|
|
14285
14811
|
}
|