stageflow 0.42.0 → 0.44.0
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/README.md +12 -3
- package/dist/agent/activity.d.ts +18 -0
- package/dist/agent/claudeAdapter.js +7 -0
- package/dist/agent/claudeTools.d.ts +27 -18
- package/dist/agent/claudeTools.js +54 -15
- package/dist/agent/compactionEvents.d.ts +34 -0
- package/dist/agent/compactionEvents.js +37 -0
- package/dist/agent/openRouterRouting.d.ts +7 -0
- package/dist/agent/openRouterRouting.js +23 -0
- package/dist/agent/piAdapter.d.ts +6 -0
- package/dist/agent/piAdapter.js +128 -27
- package/dist/agent/piSettings.d.ts +7 -0
- package/dist/agent/piSettings.js +9 -0
- package/dist/agent/sandboxPiTools.d.ts +3 -1
- package/dist/agent/sandboxPiTools.js +8 -2
- package/dist/agent/stageTurnDriver.d.ts +11 -2
- package/dist/agent/stageTurnDriver.js +45 -4
- package/dist/agent/toolOutputBudget.d.ts +15 -1
- package/dist/agent/toolOutputBudget.js +18 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
- package/dist/agent/toolOutputBudgetExtension.js +40 -3
- package/dist/agents/authorChat.d.ts +1 -0
- package/dist/agents/authorChat.js +7 -0
- package/dist/agents/authorSessionStore.d.ts +3 -0
- package/dist/agents/authorSessionStore.js +17 -2
- package/dist/agents/hostContext.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.d.ts +5 -1
- package/dist/agents/runtime/piRuntime.js +46 -12
- package/dist/agents/service.js +3 -2
- package/dist/agents/sessions.js +3 -1
- package/dist/compaction/directives.d.ts +18 -0
- package/dist/compaction/directives.js +119 -0
- package/dist/compaction/extension.d.ts +20 -0
- package/dist/compaction/extension.js +78 -0
- package/dist/compaction/mask.d.ts +18 -0
- package/dist/compaction/mask.js +155 -0
- package/dist/compaction/pin.d.ts +21 -0
- package/dist/compaction/pin.js +270 -0
- package/dist/compaction/scrub.d.ts +7 -0
- package/dist/compaction/scrub.js +26 -0
- package/dist/compaction/sessionWiring.d.ts +22 -0
- package/dist/compaction/sessionWiring.js +26 -0
- package/dist/compaction/summarize.d.ts +79 -0
- package/dist/compaction/summarize.js +171 -0
- package/dist/compaction/toolPolicy.d.ts +3 -0
- package/dist/compaction/toolPolicy.js +33 -0
- package/dist/compaction/types.d.ts +45 -0
- package/dist/compaction/types.js +7 -0
- package/dist/compaction/workingState.d.ts +56 -0
- package/dist/compaction/workingState.js +198 -0
- package/dist/config/createPipeline.js +2 -0
- package/dist/config/createTrigger.js +7 -1
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
- package/dist/config/ensureCatalogScanRoot.js +6 -0
- package/dist/config/evalCatalog.js +30 -2
- package/dist/config/githubTriggerAction.d.ts +8 -0
- package/dist/config/githubTriggerAction.js +29 -0
- package/dist/config/hostConfig.js +2 -0
- package/dist/config/loadEval.js +21 -3
- package/dist/config/pipelineNeeds.d.ts +2 -0
- package/dist/config/pipelineNeeds.js +16 -3
- package/dist/config/pipelineRoute.js +5 -4
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +12 -0
- package/dist/config/validateEval.js +43 -2
- package/dist/config/writeEval.js +2 -2
- package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
- package/dist/envelope/assertEmittedArtifacts.js +43 -0
- package/dist/envelope/preEmitChecks.d.ts +3 -0
- package/dist/evals/constants.d.ts +3 -1
- package/dist/evals/constants.js +3 -1
- package/dist/evals/engine/FakeEngine.d.ts +1 -1
- package/dist/evals/engine/FakeEngine.js +2 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
- package/dist/evals/engine/promptfoo/generate.d.ts +1 -1
- package/dist/evals/engine/promptfoo/generate.js +11 -8
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +1 -1
- package/dist/evals/engine/promptfoo/rubricPrompt.js +5 -3
- package/dist/evals/manager.d.ts +13 -0
- package/dist/evals/manager.js +10 -2
- package/dist/evals/plan.d.ts +8 -1
- package/dist/evals/plan.js +28 -9
- package/dist/evals/planInternalAgent.d.ts +19 -0
- package/dist/evals/planInternalAgent.js +205 -0
- package/dist/evals/read/api.d.ts +12 -0
- package/dist/evals/read/api.js +5 -0
- package/dist/evals/read/list.js +10 -1
- package/dist/evals/read/runs.d.ts +5 -0
- package/dist/evals/read/runs.js +7 -1
- package/dist/evals/runner/caseRunner.d.ts +2 -1
- package/dist/evals/runner/caseRunner.js +16 -5
- package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
- package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
- package/dist/evals/snapshot.d.ts +10 -2
- package/dist/evals/snapshot.js +8 -3
- package/dist/evals/types.d.ts +12 -2
- package/dist/evals/types.js +4 -1
- package/dist/guide/cardContract.d.ts +133 -0
- package/dist/guide/cardContract.js +156 -0
- package/dist/guide/chat.d.ts +45 -0
- package/dist/guide/chat.js +378 -0
- package/dist/guide/knowledge/index.d.ts +14 -0
- package/dist/guide/knowledge/index.js +109 -0
- package/dist/guide/knowledge/topics/agents.d.ts +2 -0
- package/dist/guide/knowledge/topics/agents.js +58 -0
- package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
- package/dist/guide/knowledge/topics/bestPractices.js +67 -0
- package/dist/guide/knowledge/topics/connections.d.ts +2 -0
- package/dist/guide/knowledge/topics/connections.js +66 -0
- package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
- package/dist/guide/knowledge/topics/consoleMap.js +55 -0
- package/dist/guide/knowledge/topics/evals.d.ts +2 -0
- package/dist/guide/knowledge/topics/evals.js +72 -0
- package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
- package/dist/guide/knowledge/topics/limitations.js +84 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
- package/dist/guide/knowledge/topics/memories.d.ts +2 -0
- package/dist/guide/knowledge/topics/memories.js +63 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
- package/dist/guide/knowledge/topics/overview.d.ts +2 -0
- package/dist/guide/knowledge/topics/overview.js +56 -0
- package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
- package/dist/guide/knowledge/topics/pipelines.js +93 -0
- package/dist/guide/knowledge/topics/stages.d.ts +2 -0
- package/dist/guide/knowledge/topics/stages.js +54 -0
- package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
- package/dist/guide/knowledge/topics/tasks.js +52 -0
- package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
- package/dist/guide/knowledge/topics/triggers.js +68 -0
- package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
- package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
- package/dist/guide/knowledgeTools.d.ts +2 -0
- package/dist/guide/knowledgeTools.js +56 -0
- package/dist/guide/showUi.d.ts +6 -0
- package/dist/guide/showUi.js +546 -0
- package/dist/guide/threadStore.d.ts +48 -0
- package/dist/guide/threadStore.js +189 -0
- package/dist/guide/writeRoot.d.ts +11 -0
- package/dist/guide/writeRoot.js +70 -0
- package/dist/hosted/translate.js +2 -0
- package/dist/mcp/agentTools.js +12 -0
- package/dist/mcp/catalogTools.js +18 -20
- package/dist/mcp/catalogWriteTools.d.ts +8 -0
- package/dist/mcp/catalogWriteTools.js +225 -0
- package/dist/mcp/connectionTools.d.ts +3 -0
- package/dist/mcp/connectionTools.js +85 -0
- package/dist/mcp/controlTools.js +34 -1
- package/dist/mcp/deps.d.ts +9 -0
- package/dist/mcp/evalTools.js +684 -21
- package/dist/mcp/memoryHostTools.d.ts +3 -0
- package/dist/mcp/memoryHostTools.js +72 -0
- package/dist/mcp/pipelineAuthorTools.js +14 -1
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.js +77 -31
- package/dist/operatorAgent/evalGate.d.ts +22 -0
- package/dist/operatorAgent/evalGate.js +77 -0
- package/dist/operatorAgent/fakeHost.js +10 -2
- package/dist/operatorAgent/host.js +18 -2
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/internalAgents.d.ts +11 -0
- package/dist/operatorAgent/internalAgents.js +13 -0
- package/dist/operatorAgent/piModel.d.ts +5 -1
- package/dist/operatorAgent/piModel.js +15 -10
- package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
- package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
- package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
- package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
- package/dist/operatorAgent/profiles/guideTools.js +123 -0
- package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
- package/dist/operatorAgent/profiles/stageflowGuide.js +132 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
- package/dist/operatorAgent/types.d.ts +23 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/retention.d.ts +2 -1
- package/dist/runstore/retention.js +3 -7
- package/dist/runstore/runStatusClock.d.ts +17 -0
- package/dist/runstore/runStatusClock.js +19 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +45 -45
- package/dist/runstore/workspaceLayout.js +6 -1
- package/dist/runtime/artifactDiskInspect.d.ts +14 -0
- package/dist/runtime/artifactDiskInspect.js +52 -0
- package/dist/runtime/artifactPath.d.ts +24 -0
- package/dist/runtime/artifactPath.js +84 -0
- package/dist/runtime/completionCheckRunner.js +15 -43
- package/dist/runtime/githubPollSource.d.ts +22 -4
- package/dist/runtime/githubPollSource.js +93 -8
- package/dist/runtime/priorArtifactGrants.d.ts +8 -0
- package/dist/runtime/priorArtifactGrants.js +105 -0
- package/dist/runtime/runActionEligibility.d.ts +46 -0
- package/dist/runtime/runActionEligibility.js +123 -0
- package/dist/runtime/runManager.d.ts +4 -1
- package/dist/runtime/runManager.js +52 -10
- package/dist/runtime/runRetryCoordinator.d.ts +2 -14
- package/dist/runtime/runRetryCoordinator.js +3 -67
- package/dist/runtime/sandboxContainer.d.ts +7 -0
- package/dist/runtime/sandboxContainer.js +48 -6
- package/dist/runtime/sandboxDesk.d.ts +2 -1
- package/dist/runtime/sandboxDesk.js +11 -12
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageRoots.d.ts +12 -0
- package/dist/runtime/stageSandbox.d.ts +2 -1
- package/dist/runtime/stageSandbox.js +18 -2
- package/dist/runtime/stageWorker.js +20 -0
- package/dist/server/agentAuthorRoutes.js +12 -3
- package/dist/server/bootstrap.d.ts +5 -0
- package/dist/server/bootstrap.js +14 -1
- package/dist/server/evalRoutes.js +38 -9
- package/dist/server/guideRoutes.d.ts +5 -0
- package/dist/server/guideRoutes.js +166 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +35 -1
- package/dist/server/internalAgentRoutes.d.ts +6 -0
- package/dist/server/internalAgentRoutes.js +12 -0
- package/dist/server/mcpHost.js +2 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +39 -1
- package/dist/toolkit/profiles.d.ts +5 -0
- package/dist/toolkit/profiles.js +12 -0
- package/dist/tools/askOperator.js +2 -1
- package/dist/tools/emitStageEnvelope.d.ts +17 -16
- package/dist/tools/emitStageEnvelope.js +46 -32
- package/dist/tools/stageToolNames.d.ts +4 -0
- package/dist/tools/stageToolNames.js +4 -0
- package/dist/tools/workingState.d.ts +40 -0
- package/dist/tools/workingState.js +108 -0
- package/dist/tools/writeStageArtifact.d.ts +0 -9
- package/dist/tools/writeStageArtifact.js +2 -1
- package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-S5C_vemc.js} +2 -2
- package/dist/ui/assets/adapters-C0l4ruiR.js +17 -0
- package/dist/ui/assets/chat-CjnsmvqR.js +2 -0
- package/dist/ui/assets/chat-DFDd4UjS.css +2 -0
- package/dist/ui/assets/{main-CHlmTNJe.css → main-Del1GeLf.css} +1 -1
- package/dist/ui/assets/main-_yXVqcW9.js +277 -0
- package/dist/ui/chat.html +3 -4
- package/dist/ui/index.html +3 -4
- package/dist/workshop/chatTurn.js +1 -1
- package/dist/workshop/workshopPin.d.ts +3 -3
- package/dist/workshop/workshopPin.js +16 -15
- package/examples/compaction-stress/README.md +30 -0
- package/examples/compaction-stress/check-run.mjs +53 -0
- package/examples/compaction-stress/collect-evidence.yaml +60 -0
- package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
- package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
- package/examples/compaction-stress/generate-evidence.mjs +110 -0
- package/examples/compaction-stress/root-cause.yaml +26 -0
- package/examples/compaction-stress/stageflow.yaml +9 -0
- package/examples/compaction-stress/write-postmortem.yaml +40 -0
- package/examples/evals/README.md +18 -0
- package/examples/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/evals/guide-behavior.eval.yaml +35 -0
- package/examples/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/evals/guide-depth.eval.yaml +169 -0
- package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
- package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/memory/evals/guide-depth.eval.yaml +169 -0
- package/package.json +1 -1
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
- package/dist/ui/assets/chat-Df_wCpak.js +0 -5
- package/dist/ui/assets/chat-P7KQznHm.css +0 -2
- package/dist/ui/assets/main-CvVfxmMd.js +0 -271
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const RUN_CLOCK_TERMINAL_STATUSES = [
|
|
2
|
+
"succeeded",
|
|
3
|
+
"failed",
|
|
4
|
+
"cancelled",
|
|
5
|
+
];
|
|
6
|
+
const TERMINAL = new Set(RUN_CLOCK_TERMINAL_STATUSES);
|
|
7
|
+
export function isRunClockTerminal(status) {
|
|
8
|
+
return status !== undefined && TERMINAL.has(status);
|
|
9
|
+
}
|
|
10
|
+
export function nextFinishedAt(input) {
|
|
11
|
+
if (isRunClockTerminal(input.nextStatus)) {
|
|
12
|
+
if (input.currentFinishedAt != null &&
|
|
13
|
+
input.currentFinishedAt !== "") {
|
|
14
|
+
return { action: "keep" };
|
|
15
|
+
}
|
|
16
|
+
return { action: "stamp", finishedAt: input.now };
|
|
17
|
+
}
|
|
18
|
+
return { action: "clear" };
|
|
19
|
+
}
|
|
@@ -32,6 +32,7 @@ export declare class SqliteRunStore implements RunStore {
|
|
|
32
32
|
findRunByDispatchKey(dispatchKey: string): Promise<CreatedRun | undefined>;
|
|
33
33
|
private readSubmission;
|
|
34
34
|
getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
|
|
35
|
+
private applyRunStatusUpdate;
|
|
35
36
|
updateRunStatus(runId: string, status: RunStatus): Promise<void>;
|
|
36
37
|
tryUpdateRunStatus(runId: string, status: RunStatus, expectedStatus: RunStatus): Promise<boolean>;
|
|
37
38
|
patchRunWorkspaceBinding(runId: string, patch: {
|
|
@@ -15,6 +15,7 @@ import { applyPendingMigrations, assertSchemaVersion, } from "./migrations/index
|
|
|
15
15
|
import { StoreSchemaError } from "./storeSchemaError.js";
|
|
16
16
|
import { applyStorePragmas } from "./applyStorePragmas.js";
|
|
17
17
|
import { RunSubmissionExistsError } from "../submission.js";
|
|
18
|
+
import { nextFinishedAt } from "../runStatusClock.js";
|
|
18
19
|
const EXECUTION_SELECT_COLS = `run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json, auto_resume_count`;
|
|
19
20
|
const ensuredStageDirs = new Set();
|
|
20
21
|
function executionFromRow(row) {
|
|
@@ -289,58 +290,57 @@ export class SqliteRunStore {
|
|
|
289
290
|
await this.ready();
|
|
290
291
|
return this.readSubmission(key);
|
|
291
292
|
}
|
|
293
|
+
applyRunStatusUpdate(runId, status, expectedStatus) {
|
|
294
|
+
const now = new Date().toISOString();
|
|
295
|
+
const current = this.db
|
|
296
|
+
.prepare(`SELECT finished_at FROM runs WHERE run_id = ?`)
|
|
297
|
+
.get(runId);
|
|
298
|
+
if (current === undefined) {
|
|
299
|
+
throw new Error(`Run not found: ${runId}`);
|
|
300
|
+
}
|
|
301
|
+
const decision = nextFinishedAt({
|
|
302
|
+
nextStatus: status,
|
|
303
|
+
currentFinishedAt: current.finished_at,
|
|
304
|
+
now,
|
|
305
|
+
});
|
|
306
|
+
const params = {
|
|
307
|
+
run_id: runId,
|
|
308
|
+
status,
|
|
309
|
+
updated_at: now,
|
|
310
|
+
};
|
|
311
|
+
let finishedSql;
|
|
312
|
+
if (decision.action === "stamp") {
|
|
313
|
+
finishedSql = "finished_at = @finished_at";
|
|
314
|
+
params.finished_at = decision.finishedAt;
|
|
315
|
+
}
|
|
316
|
+
else if (decision.action === "clear") {
|
|
317
|
+
finishedSql = "finished_at = NULL";
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
finishedSql = "finished_at = finished_at";
|
|
321
|
+
}
|
|
322
|
+
const expectedClause = expectedStatus !== undefined
|
|
323
|
+
? " AND status = @expected_status"
|
|
324
|
+
: "";
|
|
325
|
+
if (expectedStatus !== undefined) {
|
|
326
|
+
params.expected_status = expectedStatus;
|
|
327
|
+
}
|
|
328
|
+
const result = this.db
|
|
329
|
+
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at, ${finishedSql}
|
|
330
|
+
WHERE run_id = @run_id${expectedClause}`)
|
|
331
|
+
.run(params);
|
|
332
|
+
return result.changes;
|
|
333
|
+
}
|
|
292
334
|
async updateRunStatus(runId, status) {
|
|
293
335
|
await this.ready();
|
|
294
|
-
|
|
295
|
-
const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
|
|
296
|
-
const result = isTerminal
|
|
297
|
-
? this.db
|
|
298
|
-
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
|
|
299
|
-
finished_at = COALESCE(finished_at, @finished_at)
|
|
300
|
-
WHERE run_id = @run_id`)
|
|
301
|
-
.run({
|
|
302
|
-
run_id: runId,
|
|
303
|
-
status,
|
|
304
|
-
updated_at: now,
|
|
305
|
-
finished_at: now,
|
|
306
|
-
})
|
|
307
|
-
: this.db
|
|
308
|
-
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at WHERE run_id = @run_id`)
|
|
309
|
-
.run({
|
|
310
|
-
run_id: runId,
|
|
311
|
-
status,
|
|
312
|
-
updated_at: now,
|
|
313
|
-
});
|
|
314
|
-
if (result.changes === 0) {
|
|
336
|
+
if (this.applyRunStatusUpdate(runId, status, undefined) === 0) {
|
|
315
337
|
throw new Error(`Run not found: ${runId}`);
|
|
316
338
|
}
|
|
317
339
|
}
|
|
318
340
|
async tryUpdateRunStatus(runId, status, expectedStatus) {
|
|
319
341
|
await this.ready();
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
const result = isTerminal
|
|
323
|
-
? this.db
|
|
324
|
-
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
|
|
325
|
-
finished_at = COALESCE(finished_at, @finished_at)
|
|
326
|
-
WHERE run_id = @run_id AND status = @expected_status`)
|
|
327
|
-
.run({
|
|
328
|
-
run_id: runId,
|
|
329
|
-
status,
|
|
330
|
-
expected_status: expectedStatus,
|
|
331
|
-
updated_at: now,
|
|
332
|
-
finished_at: now,
|
|
333
|
-
})
|
|
334
|
-
: this.db
|
|
335
|
-
.prepare(`UPDATE runs SET status = @status, updated_at = @updated_at
|
|
336
|
-
WHERE run_id = @run_id AND status = @expected_status`)
|
|
337
|
-
.run({
|
|
338
|
-
run_id: runId,
|
|
339
|
-
status,
|
|
340
|
-
expected_status: expectedStatus,
|
|
341
|
-
updated_at: now,
|
|
342
|
-
});
|
|
343
|
-
if (result.changes > 0)
|
|
342
|
+
const changes = this.applyRunStatusUpdate(runId, status, expectedStatus);
|
|
343
|
+
if (changes > 0)
|
|
344
344
|
return true;
|
|
345
345
|
const exists = this.db
|
|
346
346
|
.prepare(`SELECT 1 AS ok FROM runs WHERE run_id = ?`)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, lstat, mkdir, readdir, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { globalCredentialRoot } from "../project/globalHome.js";
|
|
4
|
+
import { buildArtifactListedPath } from "../runtime/artifactPath.js";
|
|
4
5
|
import { STAGEFLOW_AGENT_AUTH_PATH_ENV } from "../runtime/credentialBinding.js";
|
|
5
6
|
import { SF_STAGE_WORKER } from "../runtime/stageWorkerProtocol.js";
|
|
6
7
|
export { stageDir } from "./paths.js";
|
|
@@ -186,7 +187,11 @@ export function resolveArtifactTarget(runWorkspaceDir, stageId, attempt, relativ
|
|
|
186
187
|
path.isAbsolute(relativeToArtifacts)) {
|
|
187
188
|
throw new Error("path escapes the stage artifacts directory");
|
|
188
189
|
}
|
|
189
|
-
const runRelativePath =
|
|
190
|
+
const runRelativePath = buildArtifactListedPath({
|
|
191
|
+
stageId,
|
|
192
|
+
attempt,
|
|
193
|
+
relative: relativeToArtifacts.split(path.sep).join("/"),
|
|
194
|
+
});
|
|
190
195
|
return {
|
|
191
196
|
absolutePath: resolved,
|
|
192
197
|
runRelativePath,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ArtifactDiskFileType = "file" | "directory" | "symlink" | "other" | "missing";
|
|
2
|
+
export type ArtifactDiskPresence = {
|
|
3
|
+
file_type: ArtifactDiskFileType;
|
|
4
|
+
/** Lexical absolute path under root when the walk reached a leaf. */
|
|
5
|
+
absolutePath?: string;
|
|
6
|
+
/** From lstat when `file_type` is `file`. */
|
|
7
|
+
size_bytes?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Closed-posture walk of `relativePath` under `rootDir`: lstat every segment,
|
|
11
|
+
* classify symlink / missing / file / directory / other. Does not follow
|
|
12
|
+
* symlinks. Escape attempts are `missing`. Non-ENOENT errors propagate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function inspectArtifactUnderRoot(rootDir: string, relativePath: string): Promise<ArtifactDiskPresence>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Closed-posture walk of `relativePath` under `rootDir`: lstat every segment,
|
|
5
|
+
* classify symlink / missing / file / directory / other. Does not follow
|
|
6
|
+
* symlinks. Escape attempts are `missing`. Non-ENOENT errors propagate.
|
|
7
|
+
*/
|
|
8
|
+
export async function inspectArtifactUnderRoot(rootDir, relativePath) {
|
|
9
|
+
const target = path.resolve(rootDir, relativePath);
|
|
10
|
+
const relative = path.relative(rootDir, target);
|
|
11
|
+
if (relative === "" ||
|
|
12
|
+
relative === ".." ||
|
|
13
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
14
|
+
path.isAbsolute(relative)) {
|
|
15
|
+
return { file_type: "missing" };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const segments = relative.split(path.sep).filter((segment) => segment !== "");
|
|
19
|
+
let current = rootDir;
|
|
20
|
+
let stats;
|
|
21
|
+
for (const segment of segments) {
|
|
22
|
+
current = path.join(current, segment);
|
|
23
|
+
stats = await lstat(current);
|
|
24
|
+
if (stats.isSymbolicLink()) {
|
|
25
|
+
return { file_type: "symlink", absolutePath: current };
|
|
26
|
+
}
|
|
27
|
+
if (current !== target && !stats.isDirectory()) {
|
|
28
|
+
return { file_type: "other", absolutePath: current };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (stats === undefined) {
|
|
32
|
+
return { file_type: "missing" };
|
|
33
|
+
}
|
|
34
|
+
if (stats.isDirectory()) {
|
|
35
|
+
return { file_type: "directory", absolutePath: current };
|
|
36
|
+
}
|
|
37
|
+
if (!stats.isFile()) {
|
|
38
|
+
return { file_type: "other", absolutePath: current };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
file_type: "file",
|
|
42
|
+
absolutePath: current,
|
|
43
|
+
size_bytes: stats.size,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === "ENOENT") {
|
|
48
|
+
return { file_type: "missing" };
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Parts of a run-relative artifact path under `stages/.../artifacts/`. */
|
|
2
|
+
export type ArtifactPathParts = {
|
|
3
|
+
stageId: string;
|
|
4
|
+
attempt: number;
|
|
5
|
+
relative: string;
|
|
6
|
+
};
|
|
7
|
+
export type ArtifactPathMode = {
|
|
8
|
+
kind: "grantable";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "current_attempt";
|
|
11
|
+
stageId: string;
|
|
12
|
+
attempt: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function attemptArtifactsPrefix(stageId: string, attempt: number): string;
|
|
15
|
+
export declare function buildArtifactListedPath(parts: ArtifactPathParts): string;
|
|
16
|
+
/**
|
|
17
|
+
* Accept a listed artifact path under a mode.
|
|
18
|
+
* - `grantable`: full run-relative path to a prior stage/attempt artifact file.
|
|
19
|
+
* - `current_attempt`: that attempt's run-relative prefix, or a bare path under
|
|
20
|
+
* its artifacts dir; foreign `stages/...` paths are rejected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function acceptArtifactListedPath(listedPath: string, mode: ArtifactPathMode): ArtifactPathParts | undefined;
|
|
23
|
+
export declare function parseArtifactListedPath(listedPath: string): ArtifactPathParts | undefined;
|
|
24
|
+
export declare function isGrantableArtifactPath(listedPath: string): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function attemptArtifactsPrefix(stageId, attempt) {
|
|
3
|
+
return path.posix.join("stages", stageId, "attempts", String(attempt), "artifacts");
|
|
4
|
+
}
|
|
5
|
+
export function buildArtifactListedPath(parts) {
|
|
6
|
+
const relative = parts.relative.replace(/\\/g, "/");
|
|
7
|
+
const segments = relative.split("/").filter((segment) => segment.length > 0);
|
|
8
|
+
return path.posix.join("stages", parts.stageId, "attempts", String(parts.attempt), "artifacts", ...segments);
|
|
9
|
+
}
|
|
10
|
+
function parseStrictGrantable(listedPath) {
|
|
11
|
+
if (typeof listedPath !== "string" || listedPath.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const posix = listedPath.replace(/\\/g, "/");
|
|
14
|
+
if (path.isAbsolute(listedPath) || path.posix.isAbsolute(posix))
|
|
15
|
+
return undefined;
|
|
16
|
+
if (posix.includes("\0") || posix.includes(".."))
|
|
17
|
+
return undefined;
|
|
18
|
+
const parts = posix.split("/").filter((part) => part.length > 0);
|
|
19
|
+
if (parts.length < 6)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (parts[0] !== "stages" || parts[2] !== "attempts" || parts[4] !== "artifacts") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const attemptRaw = parts[3];
|
|
25
|
+
if (!/^\d+$/.test(attemptRaw))
|
|
26
|
+
return undefined;
|
|
27
|
+
const attempt = Number(attemptRaw);
|
|
28
|
+
if (!Number.isInteger(attempt) || attempt < 1)
|
|
29
|
+
return undefined;
|
|
30
|
+
const relative = parts.slice(5).join("/");
|
|
31
|
+
if (relative.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
stageId: parts[1],
|
|
35
|
+
attempt,
|
|
36
|
+
relative,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Accept a listed artifact path under a mode.
|
|
41
|
+
* - `grantable`: full run-relative path to a prior stage/attempt artifact file.
|
|
42
|
+
* - `current_attempt`: that attempt's run-relative prefix, or a bare path under
|
|
43
|
+
* its artifacts dir; foreign `stages/...` paths are rejected.
|
|
44
|
+
*/
|
|
45
|
+
export function acceptArtifactListedPath(listedPath, mode) {
|
|
46
|
+
if (mode.kind === "grantable") {
|
|
47
|
+
return parseStrictGrantable(listedPath);
|
|
48
|
+
}
|
|
49
|
+
if (typeof listedPath !== "string" || listedPath.length === 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const posixListed = listedPath.replace(/\\/g, "/");
|
|
53
|
+
if (path.isAbsolute(listedPath) || path.posix.isAbsolute(posixListed)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const prefix = attemptArtifactsPrefix(mode.stageId, mode.attempt);
|
|
57
|
+
if (posixListed === prefix || posixListed === `${prefix}/`) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (posixListed.startsWith(`${prefix}/`)) {
|
|
61
|
+
const relative = posixListed.slice(prefix.length + 1);
|
|
62
|
+
if (relative.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
return {
|
|
65
|
+
stageId: mode.stageId,
|
|
66
|
+
attempt: mode.attempt,
|
|
67
|
+
relative,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (posixListed.startsWith("stages/")) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
stageId: mode.stageId,
|
|
75
|
+
attempt: mode.attempt,
|
|
76
|
+
relative: posixListed,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function parseArtifactListedPath(listedPath) {
|
|
80
|
+
return parseStrictGrantable(listedPath);
|
|
81
|
+
}
|
|
82
|
+
export function isGrantableArtifactPath(listedPath) {
|
|
83
|
+
return acceptArtifactListedPath(listedPath, { kind: "grantable" }) !== undefined;
|
|
84
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
|
|
6
6
|
import { resolveBashPath } from "../preflight/bash.js";
|
|
7
7
|
import { redactString } from "../logging/redact.js";
|
|
8
8
|
import { getNamedSecrets } from "../logging/namedSecrets.js";
|
|
9
|
+
import { inspectArtifactUnderRoot } from "./artifactDiskInspect.js";
|
|
9
10
|
import { signalProcessGroup } from "./stageProcessLauncher.js";
|
|
10
11
|
const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
11
12
|
const COMMAND_KILL_ESCALATE_MS = 2000;
|
|
@@ -163,56 +164,27 @@ async function runCommandCheck(check, input) {
|
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
async function inspectArtifact(artifactsDir, relativePath) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
167
|
+
const presence = await inspectArtifactUnderRoot(artifactsDir, relativePath);
|
|
168
|
+
if (presence.file_type === "missing") {
|
|
169
169
|
return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
// lstat every component rather than just the target: lstat(target) follows
|
|
173
|
-
// a symlink in a parent directory, which could otherwise prove a file that
|
|
174
|
-
// lives outside the attempt artifact root.
|
|
175
|
-
const segments = relative.split(path.sep).filter((segment) => segment !== "");
|
|
176
|
-
let current = artifactsDir;
|
|
177
|
-
let stats;
|
|
178
|
-
for (const segment of segments) {
|
|
179
|
-
current = path.join(current, segment);
|
|
180
|
-
stats = await lstat(current);
|
|
181
|
-
if (stats.isSymbolicLink()) {
|
|
182
|
-
return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
|
|
183
|
-
}
|
|
184
|
-
if (current !== target && !stats.isDirectory()) {
|
|
185
|
-
return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (stats === undefined) {
|
|
189
|
-
return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
|
|
190
|
-
}
|
|
191
|
-
if (stats.isSymbolicLink()) {
|
|
192
|
-
return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
|
|
193
|
-
}
|
|
194
|
-
if (stats.isDirectory()) {
|
|
195
|
-
return { kind: "artifact", path: relativePath, exists: true, file_type: "directory" };
|
|
196
|
-
}
|
|
197
|
-
if (!stats.isFile()) {
|
|
198
|
-
return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
|
|
199
|
-
}
|
|
200
|
-
const content = await readFile(target);
|
|
171
|
+
if (presence.file_type !== "file" || presence.absolutePath === undefined) {
|
|
201
172
|
return {
|
|
202
173
|
kind: "artifact",
|
|
203
174
|
path: relativePath,
|
|
204
175
|
exists: true,
|
|
205
|
-
file_type:
|
|
206
|
-
size_bytes: stats.size,
|
|
207
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
176
|
+
file_type: presence.file_type,
|
|
208
177
|
};
|
|
209
178
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
179
|
+
const content = await readFile(presence.absolutePath);
|
|
180
|
+
return {
|
|
181
|
+
kind: "artifact",
|
|
182
|
+
path: relativePath,
|
|
183
|
+
exists: true,
|
|
184
|
+
file_type: "file",
|
|
185
|
+
size_bytes: presence.size_bytes,
|
|
186
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
187
|
+
};
|
|
216
188
|
}
|
|
217
189
|
async function runArtifactCheck(check, input) {
|
|
218
190
|
try {
|
|
@@ -15,6 +15,7 @@ export type GithubPullItem = {
|
|
|
15
15
|
user: {
|
|
16
16
|
login: string;
|
|
17
17
|
} | null;
|
|
18
|
+
head_sha?: string | null;
|
|
18
19
|
};
|
|
19
20
|
export type GithubPollResponse = {
|
|
20
21
|
status: number;
|
|
@@ -35,14 +36,28 @@ export type GithubPollClient = {
|
|
|
35
36
|
};
|
|
36
37
|
export declare function defaultCreateGithubClient(token: string): GithubPollClient;
|
|
37
38
|
export type NormalizedGithubEvent = {
|
|
38
|
-
action: "opened" | "
|
|
39
|
+
action: "opened" | "synchronize" | "updated" | "merged" | "closed";
|
|
39
40
|
number: number;
|
|
40
41
|
title: string;
|
|
41
42
|
url: string;
|
|
42
43
|
author: string;
|
|
43
44
|
repo: string;
|
|
44
45
|
updatedAt: string;
|
|
46
|
+
headSha?: string;
|
|
45
47
|
};
|
|
48
|
+
type PollCursor = {
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
number: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Classify a PR list delta into a normalized GitHub action.
|
|
54
|
+
*
|
|
55
|
+
* `opened` is reserved for brand-new PRs: equal created/updated timestamps, or
|
|
56
|
+
* first sight of a number whose `created_at` is after the poll cursor. A missing
|
|
57
|
+
* prior head SHA alone is not enough — that also happens on upgrade (no `heads`
|
|
58
|
+
* row yet), eviction, or a cold PR falling into the poll window.
|
|
59
|
+
*/
|
|
60
|
+
export declare function deriveGithubAction(item: GithubPullItem, previousHeadSha: string | undefined, cursor?: PollCursor | null): NormalizedGithubEvent["action"];
|
|
46
61
|
export type GithubPollSourceOptions = {
|
|
47
62
|
store: RunStore;
|
|
48
63
|
cwd?: string;
|
|
@@ -59,9 +74,11 @@ export type GithubPollSourceOptions = {
|
|
|
59
74
|
* per tick regardless of how many triggers watch it), and diffs each repo's
|
|
60
75
|
* PR list against a persisted ETag + cursor to find genuinely new activity.
|
|
61
76
|
*
|
|
62
|
-
* The first tick for a never-before-seen repo only seeds the cursor
|
|
63
|
-
* never fires for a repo's pre-existing PR history,
|
|
64
|
-
* `ScheduleSource` seeds `next_run_at` without firing on
|
|
77
|
+
* The first tick for a never-before-seen repo only seeds the cursor and
|
|
78
|
+
* per-PR head SHAs — it never fires for a repo's pre-existing PR history,
|
|
79
|
+
* mirroring how `ScheduleSource` seeds `next_run_at` without firing on
|
|
80
|
+
* first sight. The same quiet baseline runs when a cursor already exists
|
|
81
|
+
* but `heads` has never been written (upgrade from a pre-heads store).
|
|
65
82
|
*/
|
|
66
83
|
export declare class GithubPollSource implements TriggerSourcePort {
|
|
67
84
|
private readonly store;
|
|
@@ -84,3 +101,4 @@ export declare class GithubPollSource implements TriggerSourcePort {
|
|
|
84
101
|
private pollRepo;
|
|
85
102
|
private fireSafely;
|
|
86
103
|
}
|
|
104
|
+
export {};
|
|
@@ -50,31 +50,99 @@ export function defaultCreateGithubClient(token) {
|
|
|
50
50
|
created_at: pr.created_at,
|
|
51
51
|
updated_at: pr.updated_at,
|
|
52
52
|
user: pr.user ? { login: pr.user.login } : null,
|
|
53
|
+
head_sha: pr.head?.sha ?? null,
|
|
53
54
|
})),
|
|
54
55
|
};
|
|
55
56
|
},
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
const HEADS_LIMIT = 200;
|
|
60
|
+
function isCreatedAfterCursor(item, cursor) {
|
|
61
|
+
const createdMs = new Date(item.created_at).getTime();
|
|
62
|
+
const cursorMs = new Date(cursor.updatedAt).getTime();
|
|
63
|
+
if (createdMs !== cursorMs)
|
|
64
|
+
return createdMs > cursorMs;
|
|
65
|
+
return item.number > cursor.number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classify a PR list delta into a normalized GitHub action.
|
|
69
|
+
*
|
|
70
|
+
* `opened` is reserved for brand-new PRs: equal created/updated timestamps, or
|
|
71
|
+
* first sight of a number whose `created_at` is after the poll cursor. A missing
|
|
72
|
+
* prior head SHA alone is not enough — that also happens on upgrade (no `heads`
|
|
73
|
+
* row yet), eviction, or a cold PR falling into the poll window.
|
|
74
|
+
*/
|
|
75
|
+
export function deriveGithubAction(item, previousHeadSha, cursor = null) {
|
|
59
76
|
if (item.merged_at)
|
|
60
77
|
return "merged";
|
|
61
78
|
if (item.state === "closed")
|
|
62
79
|
return "closed";
|
|
63
80
|
if (item.created_at === item.updated_at)
|
|
64
81
|
return "opened";
|
|
82
|
+
if (previousHeadSha === undefined) {
|
|
83
|
+
if (cursor !== null && isCreatedAfterCursor(item, cursor))
|
|
84
|
+
return "opened";
|
|
85
|
+
return "updated";
|
|
86
|
+
}
|
|
87
|
+
const currentSha = item.head_sha ?? null;
|
|
88
|
+
if (previousHeadSha !== "" &&
|
|
89
|
+
currentSha !== null &&
|
|
90
|
+
currentSha !== previousHeadSha) {
|
|
91
|
+
return "synchronize";
|
|
92
|
+
}
|
|
65
93
|
return "updated";
|
|
66
94
|
}
|
|
67
|
-
function normalizeGithubItem(item, repo) {
|
|
95
|
+
function normalizeGithubItem(item, repo, previousHeadSha, cursor) {
|
|
96
|
+
const headSha = item.head_sha ?? null;
|
|
68
97
|
return {
|
|
69
|
-
action:
|
|
98
|
+
action: deriveGithubAction(item, previousHeadSha, cursor),
|
|
70
99
|
number: item.number,
|
|
71
100
|
title: item.title,
|
|
72
101
|
url: item.html_url,
|
|
73
102
|
author: item.user?.login ?? "unknown",
|
|
74
103
|
repo,
|
|
75
104
|
updatedAt: item.updated_at,
|
|
105
|
+
...(headSha ? { headSha } : {}),
|
|
76
106
|
};
|
|
77
107
|
}
|
|
108
|
+
function parseHeads(raw) {
|
|
109
|
+
if (raw === null)
|
|
110
|
+
return {};
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(raw);
|
|
113
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
114
|
+
return {};
|
|
115
|
+
const out = {};
|
|
116
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
117
|
+
if (typeof value === "string")
|
|
118
|
+
out[key] = value;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function seedHeadsFromItems(items) {
|
|
127
|
+
const heads = {};
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
const sha = item.head_sha ?? null;
|
|
130
|
+
if (sha)
|
|
131
|
+
heads[String(item.number)] = sha;
|
|
132
|
+
}
|
|
133
|
+
return heads;
|
|
134
|
+
}
|
|
135
|
+
function boundHeads(heads, currentItems) {
|
|
136
|
+
if (Object.keys(heads).length <= HEADS_LIMIT)
|
|
137
|
+
return heads;
|
|
138
|
+
const currentNumbers = new Set(currentItems.map((item) => String(item.number)));
|
|
139
|
+
const next = {};
|
|
140
|
+
for (const [key, value] of Object.entries(heads)) {
|
|
141
|
+
if (currentNumbers.has(key))
|
|
142
|
+
next[key] = value;
|
|
143
|
+
}
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
78
146
|
function parseCursor(raw) {
|
|
79
147
|
if (raw === null)
|
|
80
148
|
return null;
|
|
@@ -111,9 +179,11 @@ function firstSecretRef(matches) {
|
|
|
111
179
|
* per tick regardless of how many triggers watch it), and diffs each repo's
|
|
112
180
|
* PR list against a persisted ETag + cursor to find genuinely new activity.
|
|
113
181
|
*
|
|
114
|
-
* The first tick for a never-before-seen repo only seeds the cursor
|
|
115
|
-
* never fires for a repo's pre-existing PR history,
|
|
116
|
-
* `ScheduleSource` seeds `next_run_at` without firing on
|
|
182
|
+
* The first tick for a never-before-seen repo only seeds the cursor and
|
|
183
|
+
* per-PR head SHAs — it never fires for a repo's pre-existing PR history,
|
|
184
|
+
* mirroring how `ScheduleSource` seeds `next_run_at` without firing on
|
|
185
|
+
* first sight. The same quiet baseline runs when a cursor already exists
|
|
186
|
+
* but `heads` has never been written (upgrade from a pre-heads store).
|
|
117
187
|
*/
|
|
118
188
|
export class GithubPollSource {
|
|
119
189
|
store;
|
|
@@ -217,6 +287,8 @@ export class GithubPollSource {
|
|
|
217
287
|
const adapterId = `github:${repo}`;
|
|
218
288
|
const etag = await this.store.getTriggerAdapterState(adapterId, "etag");
|
|
219
289
|
const cursor = parseCursor(await this.store.getTriggerAdapterState(adapterId, "cursor"));
|
|
290
|
+
const headsRaw = await this.store.getTriggerAdapterState(adapterId, "heads");
|
|
291
|
+
let heads = parseHeads(headsRaw);
|
|
220
292
|
const [owner, repoName] = repo.split("/");
|
|
221
293
|
let response;
|
|
222
294
|
try {
|
|
@@ -239,13 +311,24 @@ export class GithubPollSource {
|
|
|
239
311
|
if (response.items.length === 0)
|
|
240
312
|
return;
|
|
241
313
|
const newest = response.items[0];
|
|
242
|
-
|
|
314
|
+
// First sight, or upgrade from a pre-heads store (cursor present, heads absent):
|
|
315
|
+
// baseline without firing so aged PRs are not misclassified as opened.
|
|
316
|
+
if (cursor === null || headsRaw === null) {
|
|
317
|
+
heads = seedHeadsFromItems(response.items);
|
|
318
|
+
await this.store.setTriggerAdapterState(adapterId, "heads", JSON.stringify(heads));
|
|
243
319
|
await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
|
|
244
320
|
return;
|
|
245
321
|
}
|
|
246
322
|
const newItems = response.items.filter((item) => isNewerThanCursor(item, cursor)).reverse();
|
|
247
323
|
for (const item of newItems) {
|
|
248
|
-
const
|
|
324
|
+
const numberKey = String(item.number);
|
|
325
|
+
const previousHeadSha = Object.prototype.hasOwnProperty.call(heads, numberKey) ? heads[numberKey] : undefined;
|
|
326
|
+
const summary = normalizeGithubItem(item, repo, previousHeadSha, cursor);
|
|
327
|
+
const nextSha = item.head_sha ?? null;
|
|
328
|
+
if (nextSha)
|
|
329
|
+
heads[numberKey] = nextSha;
|
|
330
|
+
else if (previousHeadSha !== undefined && previousHeadSha !== "")
|
|
331
|
+
heads[numberKey] = previousHeadSha;
|
|
249
332
|
for (const match of matches) {
|
|
250
333
|
const { definition } = match;
|
|
251
334
|
if (!matchesEventFilter(definition.event?.match, summary))
|
|
@@ -253,6 +336,8 @@ export class GithubPollSource {
|
|
|
253
336
|
await this.fireSafely(onFire, match, summary);
|
|
254
337
|
}
|
|
255
338
|
}
|
|
339
|
+
heads = boundHeads(heads, response.items);
|
|
340
|
+
await this.store.setTriggerAdapterState(adapterId, "heads", JSON.stringify(heads));
|
|
256
341
|
await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
|
|
257
342
|
}
|
|
258
343
|
async fireSafely(onFire, match, summary) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvePriorEnvelopeResult } from "./envelopeRouting.js";
|
|
2
|
+
import type { StageArtifactGrant } from "./stageRoots.js";
|
|
3
|
+
export { isGrantableArtifactPath } from "./artifactPath.js";
|
|
4
|
+
/** Sandbox path for one artifact. Colliding names from different attempts include the attempt. */
|
|
5
|
+
export declare function assignGrantContainerPaths(listedPaths: readonly string[]): Map<string, string>;
|
|
6
|
+
export declare function artifactGrantsFromPriors(workspaceDir: string, prior: Extract<ResolvePriorEnvelopeResult, {
|
|
7
|
+
ok: true;
|
|
8
|
+
}>): Promise<StageArtifactGrant[]>;
|