@shipfox/api-workflows 13.0.0 → 14.0.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +46 -0
- package/README.md +12 -5
- package/dist/core/checkout.d.ts +4 -2
- package/dist/core/checkout.d.ts.map +1 -1
- package/dist/core/checkout.js +7 -2
- package/dist/core/checkout.js.map +1 -1
- package/dist/core/entities/job.d.ts +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +23 -4
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/errors.d.ts +9 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +5 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/job-execution.d.ts.map +1 -1
- package/dist/core/job-execution.js +12 -2
- package/dist/core/job-execution.js.map +1 -1
- package/dist/core/run-workflow.js +1 -1
- package/dist/core/run-workflow.js.map +1 -1
- package/dist/core/step-config/agent.d.ts.map +1 -1
- package/dist/core/step-config/agent.js +9 -1
- package/dist/core/step-config/agent.js.map +1 -1
- package/dist/db/db.d.ts +72 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.d.ts +45 -1
- package/dist/db/schema/workflow-runs.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.js +42 -1
- package/dist/db/schema/workflow-runs.js.map +1 -1
- package/dist/db/workflow-runs/jobs.d.ts +3 -1
- package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
- package/dist/db/workflow-runs/jobs.js +12 -3
- package/dist/db/workflow-runs/jobs.js.map +1 -1
- package/dist/db/workflow-runs/queries.d.ts +3 -2
- package/dist/db/workflow-runs/queries.d.ts.map +1 -1
- package/dist/db/workflow-runs/queries.js +3 -0
- package/dist/db/workflow-runs/queries.js.map +1 -1
- package/dist/presentation/dto/step.d.ts.map +1 -1
- package/dist/presentation/dto/step.js +14 -0
- package/dist/presentation/dto/step.js.map +1 -1
- package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
- package/dist/presentation/dto/workflow-run.js +14 -0
- package/dist/presentation/dto/workflow-run.js.map +1 -1
- package/dist/presentation/routes/agent-runtime-config.d.ts.map +1 -1
- package/dist/presentation/routes/agent-runtime-config.js +22 -0
- package/dist/presentation/routes/agent-runtime-config.js.map +1 -1
- package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
- package/dist/presentation/routes/checkout-token.js +2 -1
- package/dist/presentation/routes/checkout-token.js.map +1 -1
- package/dist/presentation/routes/get-run-aggregates.d.ts.map +1 -1
- package/dist/presentation/routes/get-run-aggregates.js +2 -1
- package/dist/presentation/routes/get-run-aggregates.js.map +1 -1
- package/dist/presentation/routes/leased-step.d.ts +3 -1
- package/dist/presentation/routes/leased-step.d.ts.map +1 -1
- package/dist/presentation/routes/leased-step.js +2 -1
- package/dist/presentation/routes/leased-step.js.map +1 -1
- package/dist/presentation/routes/list-runs.d.ts.map +1 -1
- package/dist/presentation/routes/list-runs.js +2 -1
- package/dist/presentation/routes/list-runs.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_dev_run_origin.sql +12 -0
- package/drizzle/meta/0004_snapshot.json +1773 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +10 -10
- package/src/core/checkout.test.ts +198 -1
- package/src/core/checkout.ts +18 -5
- package/src/core/entities/job.ts +1 -1
- package/src/core/entities/workflow-run.ts +22 -4
- package/src/core/errors.ts +17 -2
- package/src/core/job-execution.test.ts +54 -0
- package/src/core/job-execution.ts +6 -1
- package/src/core/job-transition/decide-job-activation.test.ts +2 -0
- package/src/core/run-workflow.test.ts +50 -1
- package/src/core/run-workflow.ts +1 -1
- package/src/core/step-config/agent.ts +8 -1
- package/src/core/step-config/complete-step-dispatch-config.test.ts +37 -0
- package/src/core/workflow-run-creation.test.ts +2 -0
- package/src/db/job-listeners.test.ts +17 -0
- package/src/db/schema/workflow-runs.ts +58 -0
- package/src/db/workflow-runs/jobs.ts +17 -3
- package/src/db/workflow-runs/queries.ts +6 -1
- package/src/db/workflow-runs/run-create.test.ts +39 -0
- package/src/presentation/dto/step.test.ts +8 -0
- package/src/presentation/dto/step.ts +9 -0
- package/src/presentation/dto/workflow-run.ts +23 -1
- package/src/presentation/routes/agent-runtime-config.test.ts +84 -2
- package/src/presentation/routes/agent-runtime-config.ts +24 -0
- package/src/presentation/routes/checkout-token.test.ts +45 -0
- package/src/presentation/routes/checkout-token.ts +2 -1
- package/src/presentation/routes/get-run-aggregates.test.ts +56 -0
- package/src/presentation/routes/get-run-aggregates.ts +2 -0
- package/src/presentation/routes/get-step-secrets.test.ts +1 -0
- package/src/presentation/routes/leased-step.ts +7 -1
- package/src/presentation/routes/list-runs.test.ts +152 -0
- package/src/presentation/routes/list-runs.ts +2 -0
- package/test/factories/workflow-run.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/core/run-workflow.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function runWorkflow(
|
|
|
44
44
|
return createWorkflowRun({
|
|
45
45
|
workspaceId: params.workspaceId,
|
|
46
46
|
projectId: params.projectId,
|
|
47
|
-
definitionId: definition.
|
|
47
|
+
definitionId: definition.workflowId,
|
|
48
48
|
name: definition.name,
|
|
49
49
|
model,
|
|
50
50
|
triggerPayload: params.triggerPayload,
|
|
@@ -185,7 +185,14 @@ export async function completeAgentDefaults(params: {
|
|
|
185
185
|
isInterModuleKnownError(agentInterModuleContract.methods.resolveAgentConfig, error) &&
|
|
186
186
|
error.code === 'agent-config-invalid'
|
|
187
187
|
) {
|
|
188
|
-
|
|
188
|
+
const managedProviderId = error.details.managed_provider_id;
|
|
189
|
+
throw new AgentConfigUnresolvableError(params.definitionId, {
|
|
190
|
+
cause: error,
|
|
191
|
+
...(error.details.message === undefined ? {} : {message: error.details.message}),
|
|
192
|
+
...(managedProviderId === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: {code: 'workspace-providers-disabled', managedProviderId}),
|
|
195
|
+
});
|
|
189
196
|
}
|
|
190
197
|
throw error;
|
|
191
198
|
}
|
|
@@ -453,6 +453,43 @@ describe('completeStepDispatchConfig', () => {
|
|
|
453
453
|
await expect(act()).rejects.toThrow(AgentConfigUnresolvableError);
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
+
it('preserves managed provider policy details in unresolvable agent config errors', async () => {
|
|
457
|
+
const pending = step({
|
|
458
|
+
type: 'agent',
|
|
459
|
+
config: {},
|
|
460
|
+
configPlan: {
|
|
461
|
+
agent: {
|
|
462
|
+
prompt: plannedField('Review the change.'),
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
const failingResolver: AgentDefaultsResolver = () => {
|
|
467
|
+
throw createInterModuleKnownError(
|
|
468
|
+
agentInterModuleContract.methods.resolveAgentConfig,
|
|
469
|
+
'agent-config-invalid',
|
|
470
|
+
{
|
|
471
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
472
|
+
managed_provider_id: 'shipfox',
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const act = () =>
|
|
478
|
+
completeStepDispatchConfig({
|
|
479
|
+
step: pending,
|
|
480
|
+
context,
|
|
481
|
+
resolveAgentDefaults: failingResolver,
|
|
482
|
+
definitionId: 'def-1',
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await expect(act()).rejects.toMatchObject({
|
|
486
|
+
name: 'AgentConfigUnresolvableError',
|
|
487
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
488
|
+
code: 'workspace-providers-disabled',
|
|
489
|
+
managedProviderId: 'shipfox',
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
456
493
|
it('throws when a server-side segment still survives dispatch', async () => {
|
|
457
494
|
const pending = step({
|
|
458
495
|
config: {},
|
|
@@ -144,6 +144,8 @@ function workflowRun(params: {inputs?: Record<string, unknown> | null} = {}): Wo
|
|
|
144
144
|
workflowName: 'Test workflow',
|
|
145
145
|
nameOverride: null,
|
|
146
146
|
status: 'pending',
|
|
147
|
+
origin: 'synced',
|
|
148
|
+
devSource: null,
|
|
147
149
|
currentAttempt: 1,
|
|
148
150
|
triggerProvider: null,
|
|
149
151
|
triggerSource: triggerPayload.source,
|
|
@@ -278,6 +278,23 @@ describe('activateJobListener', () => {
|
|
|
278
278
|
expect(result.executionCount).toBe(2);
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
it('projects matchers with the event omitted', async () => {
|
|
282
|
+
const job = await createInactiveListeningJobWithMatchers({
|
|
283
|
+
on: [{source: 'github_acme'}],
|
|
284
|
+
until: [{source: 'github_acme', filter: 'event.action == "closed"'}],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await activateJobListener({jobId: job.id, expectedVersion: job.version});
|
|
288
|
+
|
|
289
|
+
const payload = await activatedPayload(job.id);
|
|
290
|
+
expectListeningPayload(payload);
|
|
291
|
+
expect(payload.on[0]).toEqual({source: 'github_acme'});
|
|
292
|
+
expect(payload.until?.[0]).toEqual({
|
|
293
|
+
source: 'github_acme',
|
|
294
|
+
filter: 'event.action == "closed"',
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
281
298
|
it('omits filter snapshots for matchers without non-event roots', async () => {
|
|
282
299
|
const job = await createInactiveListeningJobWithMatchers({
|
|
283
300
|
on: [{source: 'github', event: 'pull_request'}],
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type {
|
|
17
17
|
TriggerPayload,
|
|
18
18
|
WorkflowRun,
|
|
19
|
+
WorkflowRunOriginState,
|
|
19
20
|
WorkflowRunTriggerReference,
|
|
20
21
|
WorkflowSourceSnapshot,
|
|
21
22
|
} from '#core/entities/workflow-run.js';
|
|
@@ -31,6 +32,14 @@ export const workflowRunStatusEnum = pgEnum('workflows_run_status', [
|
|
|
31
32
|
|
|
32
33
|
export const workflowRunRerunModeEnum = pgEnum('workflows_rerun_mode', ['all', 'failed']);
|
|
33
34
|
|
|
35
|
+
export interface WorkflowRunDevSourceDb {
|
|
36
|
+
ref: string;
|
|
37
|
+
commit: string;
|
|
38
|
+
config_path: string;
|
|
39
|
+
initiated_by_user_id: string;
|
|
40
|
+
replay_of_event_id: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
export const workflowRuns = pgTable(
|
|
35
44
|
'workflow_runs',
|
|
36
45
|
{
|
|
@@ -42,6 +51,8 @@ export const workflowRuns = pgTable(
|
|
|
42
51
|
name: text('name'),
|
|
43
52
|
workflowName: text('workflow_name').notNull(),
|
|
44
53
|
status: workflowRunStatusEnum('status').notNull().default('pending'),
|
|
54
|
+
origin: text('origin').notNull().default('synced'),
|
|
55
|
+
devSource: jsonb('dev_source').$type<WorkflowRunDevSourceDb>(),
|
|
45
56
|
currentAttempt: integer('current_attempt').notNull().default(1),
|
|
46
57
|
triggerProvider: text('trigger_provider'),
|
|
47
58
|
triggerSource: text('trigger_source').notNull(),
|
|
@@ -62,6 +73,12 @@ export const workflowRuns = pgTable(
|
|
|
62
73
|
uniqueIndex('workflows_wr_trigger_idempotency_key_unique').on(table.triggerIdempotencyKey),
|
|
63
74
|
uniqueIndex('workflows_wr_definition_number_unique').on(table.definitionId, table.number),
|
|
64
75
|
index('workflows_wr_project_created_id_idx').on(table.projectId, table.createdAt, table.id),
|
|
76
|
+
index('workflows_wr_project_origin_created_id_idx').on(
|
|
77
|
+
table.projectId,
|
|
78
|
+
table.origin,
|
|
79
|
+
table.createdAt,
|
|
80
|
+
table.id,
|
|
81
|
+
),
|
|
65
82
|
index('workflows_wr_project_status_created_id_idx').on(
|
|
66
83
|
table.projectId,
|
|
67
84
|
table.status,
|
|
@@ -85,6 +102,18 @@ export const workflowRuns = pgTable(
|
|
|
85
102
|
// historical table grows.
|
|
86
103
|
index('workflows_wr_running_idx').on(table.status).where(sql`${table.status} = 'running'`),
|
|
87
104
|
check('workflows_wr_current_attempt_positive_ck', sql`${table.currentAttempt} > 0`),
|
|
105
|
+
check('workflows_wr_origin_ck', sql`${table.origin} in ('synced', 'dev')`),
|
|
106
|
+
check(
|
|
107
|
+
'workflows_wr_dev_source_ck',
|
|
108
|
+
sql`(
|
|
109
|
+
(${table.origin} = 'synced' and ${table.devSource} is null)
|
|
110
|
+
or (
|
|
111
|
+
${table.origin} = 'dev'
|
|
112
|
+
and ${table.devSource} is not null
|
|
113
|
+
and jsonb_typeof(${table.devSource}) = 'object'
|
|
114
|
+
)
|
|
115
|
+
)`,
|
|
116
|
+
),
|
|
88
117
|
],
|
|
89
118
|
);
|
|
90
119
|
|
|
@@ -92,6 +121,7 @@ export type WorkflowRunDb = typeof workflowRuns.$inferSelect;
|
|
|
92
121
|
export type WorkflowRunCreateDb = typeof workflowRuns.$inferInsert;
|
|
93
122
|
|
|
94
123
|
export function toWorkflowRun(row: WorkflowRunDb): WorkflowRun {
|
|
124
|
+
const originState = toWorkflowRunOriginState(row);
|
|
95
125
|
return {
|
|
96
126
|
id: row.id,
|
|
97
127
|
workspaceId: row.workspaceId,
|
|
@@ -102,6 +132,7 @@ export function toWorkflowRun(row: WorkflowRunDb): WorkflowRun {
|
|
|
102
132
|
workflowName: row.workflowName,
|
|
103
133
|
nameOverride: row.name,
|
|
104
134
|
status: row.status,
|
|
135
|
+
...originState,
|
|
105
136
|
currentAttempt: row.currentAttempt,
|
|
106
137
|
triggerProvider: row.triggerProvider,
|
|
107
138
|
triggerSource: row.triggerSource,
|
|
@@ -119,3 +150,30 @@ export function toWorkflowRun(row: WorkflowRunDb): WorkflowRun {
|
|
|
119
150
|
finishedAt: row.finishedAt,
|
|
120
151
|
};
|
|
121
152
|
}
|
|
153
|
+
|
|
154
|
+
export function toWorkflowRunOriginState(
|
|
155
|
+
row: Pick<WorkflowRunDb, 'origin' | 'devSource'>,
|
|
156
|
+
): WorkflowRunOriginState {
|
|
157
|
+
if (row.origin === 'synced') {
|
|
158
|
+
if (row.devSource !== null) {
|
|
159
|
+
throw new Error(`Synced workflow run has unexpected dev source`);
|
|
160
|
+
}
|
|
161
|
+
return {origin: 'synced', devSource: null};
|
|
162
|
+
}
|
|
163
|
+
if (row.origin !== 'dev') {
|
|
164
|
+
throw new Error(`Unknown workflow run origin: ${row.origin}`);
|
|
165
|
+
}
|
|
166
|
+
if (!row.devSource) {
|
|
167
|
+
throw new Error('Dev workflow run is missing its source');
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
origin: 'dev',
|
|
171
|
+
devSource: {
|
|
172
|
+
ref: row.devSource.ref,
|
|
173
|
+
commit: row.devSource.commit,
|
|
174
|
+
configPath: row.devSource.config_path,
|
|
175
|
+
initiatedByUserId: row.devSource.initiated_by_user_id,
|
|
176
|
+
replayOfEventId: row.devSource.replay_of_event_id,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -5,7 +5,10 @@ import {and, asc, desc, eq, inArray, notInArray, sql} from 'drizzle-orm';
|
|
|
5
5
|
import {isJobTerminal, type Job, type JobStatus, type JobStatusReason} from '#core/entities/job.js';
|
|
6
6
|
import type {JobExecution} from '#core/entities/job-execution.js';
|
|
7
7
|
import type {PersistedEvaluationTraceEntry} from '#core/entities/step.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowRunOriginState,
|
|
10
|
+
WorkflowRunTriggerReference,
|
|
11
|
+
} from '#core/entities/workflow-run.js';
|
|
9
12
|
import {JobNotFoundError} from '#core/errors.js';
|
|
10
13
|
import {
|
|
11
14
|
type DeriveJobSuccessResult,
|
|
@@ -21,7 +24,7 @@ import {writeWorkflowsOutboxEvent} from '../outbox-writes.js';
|
|
|
21
24
|
import {jobExecutions, toJobExecution} from '../schema/job-executions.js';
|
|
22
25
|
import {jobs, toJob} from '../schema/jobs.js';
|
|
23
26
|
import {workflowRunAttempts} from '../schema/workflow-run-attempts.js';
|
|
24
|
-
import {toWorkflowRun, workflowRuns} from '../schema/workflow-runs.js';
|
|
27
|
+
import {toWorkflowRun, toWorkflowRunOriginState, workflowRuns} from '../schema/workflow-runs.js';
|
|
25
28
|
import {getWorkflowRunById} from './queries.js';
|
|
26
29
|
import {getWorkflowContextForJob, optimisticLockRetry, TERMINAL_JOB_STATUSES} from './shared.js';
|
|
27
30
|
|
|
@@ -71,6 +74,8 @@ export interface JobScope {
|
|
|
71
74
|
workspaceId: string;
|
|
72
75
|
projectId: string;
|
|
73
76
|
triggerReference: WorkflowRunTriggerReference | null;
|
|
77
|
+
/** The run's origin state, so checkout fallbacks can read the dev source. */
|
|
78
|
+
run: WorkflowRunOriginState;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
export async function getJobScope(jobId: string): Promise<JobScope | undefined> {
|
|
@@ -79,13 +84,22 @@ export async function getJobScope(jobId: string): Promise<JobScope | undefined>
|
|
|
79
84
|
workspaceId: workflowRuns.workspaceId,
|
|
80
85
|
projectId: workflowRuns.projectId,
|
|
81
86
|
triggerReference: workflowRuns.triggerReference,
|
|
87
|
+
origin: workflowRuns.origin,
|
|
88
|
+
devSource: workflowRuns.devSource,
|
|
82
89
|
})
|
|
83
90
|
.from(jobs)
|
|
84
91
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
85
92
|
.innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
|
|
86
93
|
.where(eq(jobs.id, jobId))
|
|
87
94
|
.limit(1);
|
|
88
|
-
|
|
95
|
+
const row = rows[0];
|
|
96
|
+
if (!row) return undefined;
|
|
97
|
+
return {
|
|
98
|
+
workspaceId: row.workspaceId,
|
|
99
|
+
projectId: row.projectId,
|
|
100
|
+
triggerReference: row.triggerReference,
|
|
101
|
+
run: toWorkflowRunOriginState(row),
|
|
102
|
+
};
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
export async function getDirectDependencyJobContexts(
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
WorkflowJobDetail,
|
|
14
14
|
WorkflowRun,
|
|
15
15
|
WorkflowRunDetail,
|
|
16
|
+
WorkflowRunOrigin,
|
|
16
17
|
WorkflowRunStatus,
|
|
17
18
|
} from '#core/entities/workflow-run.js';
|
|
18
19
|
import {db} from '../db.js';
|
|
@@ -29,6 +30,7 @@ export interface WorkflowRunFilters {
|
|
|
29
30
|
status?: WorkflowRunStatus | undefined;
|
|
30
31
|
definitionId?: string | undefined;
|
|
31
32
|
triggerSource?: string | undefined;
|
|
33
|
+
origin?: WorkflowRunOrigin | undefined;
|
|
32
34
|
createdFrom?: Date | undefined;
|
|
33
35
|
createdTo?: Date | undefined;
|
|
34
36
|
}
|
|
@@ -145,7 +147,7 @@ export function buildWorkflowRunListConditions(params: {
|
|
|
145
147
|
projectId: string;
|
|
146
148
|
filters?: WorkflowRunFilters | undefined;
|
|
147
149
|
cursor?: WorkflowRunCursor | undefined;
|
|
148
|
-
omit?: 'status' | 'definitionId' | 'triggerSource' | undefined;
|
|
150
|
+
omit?: 'status' | 'definitionId' | 'triggerSource' | 'origin' | undefined;
|
|
149
151
|
}): SQL[] {
|
|
150
152
|
const filters = params.filters;
|
|
151
153
|
const conditions: SQL[] = [eq(workflowRuns.projectId, params.projectId)];
|
|
@@ -164,6 +166,9 @@ export function buildWorkflowRunListConditions(params: {
|
|
|
164
166
|
if (filters?.triggerSource && params.omit !== 'triggerSource') {
|
|
165
167
|
conditions.push(eq(workflowRuns.triggerSource, filters.triggerSource));
|
|
166
168
|
}
|
|
169
|
+
if (filters?.origin && params.omit !== 'origin') {
|
|
170
|
+
conditions.push(eq(workflowRuns.origin, filters.origin));
|
|
171
|
+
}
|
|
167
172
|
if (filters?.createdFrom) {
|
|
168
173
|
conditions.push(gte(workflowRuns.createdAt, filters.createdFrom));
|
|
169
174
|
}
|
|
@@ -48,6 +48,45 @@ describe('workflow run queries', () => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
describe('createWorkflowRun', () => {
|
|
51
|
+
test('enforces the workflow run origin and dev source relationship at the database boundary', async () => {
|
|
52
|
+
const run = await createWorkflowRun({
|
|
53
|
+
workspaceId,
|
|
54
|
+
projectId,
|
|
55
|
+
definitionId,
|
|
56
|
+
model: buildModel(),
|
|
57
|
+
triggerPayload: {
|
|
58
|
+
source: 'manual',
|
|
59
|
+
event: 'fire',
|
|
60
|
+
subscriptionId: crypto.randomUUID(),
|
|
61
|
+
userId: crypto.randomUUID(),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await expect(
|
|
66
|
+
db().execute(
|
|
67
|
+
sql`UPDATE ${workflowRuns} SET origin = 'dev', dev_source = '{}'::jsonb WHERE ${workflowRuns.id} = ${run.id}`,
|
|
68
|
+
),
|
|
69
|
+
).resolves.toBeDefined();
|
|
70
|
+
|
|
71
|
+
await expect(
|
|
72
|
+
db().update(workflowRuns).set({origin: 'synced'}).where(eq(workflowRuns.id, run.id)),
|
|
73
|
+
).rejects.toThrow();
|
|
74
|
+
|
|
75
|
+
await db()
|
|
76
|
+
.update(workflowRuns)
|
|
77
|
+
.set({
|
|
78
|
+
origin: 'dev',
|
|
79
|
+
devSource: {
|
|
80
|
+
ref: 'main',
|
|
81
|
+
commit: 'abc123',
|
|
82
|
+
config_path: '.shipfox/workflows.yml',
|
|
83
|
+
initiated_by_user_id: crypto.randomUUID(),
|
|
84
|
+
replay_of_event_id: null,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.where(eq(workflowRuns.id, run.id));
|
|
88
|
+
});
|
|
89
|
+
|
|
51
90
|
test('persists normalized integration trigger facts alongside the payload', async () => {
|
|
52
91
|
const triggerConnectionId = crypto.randomUUID();
|
|
53
92
|
const integrations = {
|
|
@@ -43,12 +43,16 @@ describe('fromStepErrorDto', () => {
|
|
|
43
43
|
it('persists the machine-readable agent config issue in camelCase', () => {
|
|
44
44
|
const persisted = fromStepErrorDto({
|
|
45
45
|
message: 'Missing credentials',
|
|
46
|
+
code: 'workspace-providers-disabled',
|
|
47
|
+
managed_provider_id: 'shipfox',
|
|
46
48
|
reason: 'agent_config_invalid',
|
|
47
49
|
agent_config_issue: 'provider_not_configured',
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
expect(persisted).toEqual({
|
|
51
53
|
message: 'Missing credentials',
|
|
54
|
+
code: 'workspace-providers-disabled',
|
|
55
|
+
managedProviderId: 'shipfox',
|
|
52
56
|
reason: 'agent_config_invalid',
|
|
53
57
|
agentConfigIssue: 'provider_not_configured',
|
|
54
58
|
});
|
|
@@ -180,6 +184,8 @@ describe('toStepDto error category', () => {
|
|
|
180
184
|
type: 'agent',
|
|
181
185
|
error: {
|
|
182
186
|
message: 'Unknown provider "foo" for agent step.',
|
|
187
|
+
code: 'workspace-providers-disabled',
|
|
188
|
+
managedProviderId: 'shipfox',
|
|
183
189
|
reason: 'agent_config_invalid',
|
|
184
190
|
agentConfigIssue: 'provider_unsupported',
|
|
185
191
|
},
|
|
@@ -188,6 +194,8 @@ describe('toStepDto error category', () => {
|
|
|
188
194
|
|
|
189
195
|
expect(dto.error).toEqual({
|
|
190
196
|
message: 'Unknown provider "foo" for agent step.',
|
|
197
|
+
code: 'workspace-providers-disabled',
|
|
198
|
+
managed_provider_id: 'shipfox',
|
|
191
199
|
reason: 'agent_config_invalid',
|
|
192
200
|
agent_config_issue: 'provider_unsupported',
|
|
193
201
|
category: 'user',
|
|
@@ -22,6 +22,9 @@ function toStepErrorDto(
|
|
|
22
22
|
): StepErrorDto {
|
|
23
23
|
if (error === null) return null;
|
|
24
24
|
const message = typeof error.message === 'string' ? error.message : '';
|
|
25
|
+
const code = typeof error.code === 'string' ? error.code : undefined;
|
|
26
|
+
const managedProviderId =
|
|
27
|
+
typeof error.managedProviderId === 'string' ? error.managedProviderId : undefined;
|
|
25
28
|
const exitCode = error.exitCode;
|
|
26
29
|
const signal = typeof error.signal === 'string' ? error.signal : undefined;
|
|
27
30
|
const field = typeof error.field === 'string' ? error.field : undefined;
|
|
@@ -30,6 +33,8 @@ function toStepErrorDto(
|
|
|
30
33
|
const agentConfigIssue = agentConfigIssueSchema.safeParse(error.agentConfigIssue);
|
|
31
34
|
return {
|
|
32
35
|
message,
|
|
36
|
+
...(code === undefined ? {} : {code}),
|
|
37
|
+
...(managedProviderId === undefined ? {} : {managed_provider_id: managedProviderId}),
|
|
33
38
|
...(exitCode === null || typeof exitCode === 'number' ? {exit_code: exitCode} : {}),
|
|
34
39
|
...(signal === undefined ? {} : {signal}),
|
|
35
40
|
...(reason.success ? {reason: reason.data} : {}),
|
|
@@ -48,6 +53,10 @@ export function fromStepErrorDto(error: StepErrorDto | undefined): Record<string
|
|
|
48
53
|
if (!error) return null;
|
|
49
54
|
return {
|
|
50
55
|
message: error.message,
|
|
56
|
+
...(error.code === undefined ? {} : {code: error.code}),
|
|
57
|
+
...(error.managed_provider_id === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: {managedProviderId: error.managed_provider_id}),
|
|
51
60
|
...(error.exit_code === null || typeof error.exit_code === 'number'
|
|
52
61
|
? {exitCode: error.exit_code}
|
|
53
62
|
: {}),
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
WorkflowRunAttemptDto,
|
|
3
|
+
WorkflowRunDevSourceDto,
|
|
3
4
|
WorkflowRunDto,
|
|
4
5
|
WorkflowRunListItemDto,
|
|
5
6
|
WorkflowRunTriggerReferenceDto,
|
|
6
7
|
} from '@shipfox/api-workflows-dto';
|
|
7
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowRun,
|
|
10
|
+
WorkflowRunDevSource,
|
|
11
|
+
WorkflowRunTriggerReference,
|
|
12
|
+
} from '#core/entities/workflow-run.js';
|
|
8
13
|
import type {WorkflowRunAttempt} from '#core/entities/workflow-run-attempt.js';
|
|
9
14
|
import type {WorkflowRunJobsSummary} from '#db/index.js';
|
|
10
15
|
|
|
@@ -17,6 +22,8 @@ export function toRunDto(run: WorkflowRun, latestAttempt = run.currentAttempt):
|
|
|
17
22
|
name: run.name,
|
|
18
23
|
workflow_name: run.workflowName,
|
|
19
24
|
status: run.status,
|
|
25
|
+
origin: run.origin,
|
|
26
|
+
dev_source: toDevSourceDto(run.devSource),
|
|
20
27
|
current_attempt: run.currentAttempt,
|
|
21
28
|
latest_attempt: latestAttempt,
|
|
22
29
|
trigger_provider: run.triggerProvider,
|
|
@@ -76,6 +83,21 @@ function toTriggerReferenceDto(
|
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
// The persisted dev source uses camelCase field names while the API contract is snake_case,
|
|
87
|
+
// so each field is mapped explicitly rather than spread onto the response.
|
|
88
|
+
function toDevSourceDto(
|
|
89
|
+
devSource: WorkflowRunDevSource | null | undefined,
|
|
90
|
+
): WorkflowRunDevSourceDto | null {
|
|
91
|
+
if (!devSource) return null;
|
|
92
|
+
return {
|
|
93
|
+
ref: devSource.ref,
|
|
94
|
+
commit: devSource.commit,
|
|
95
|
+
config_path: devSource.configPath,
|
|
96
|
+
initiated_by_user_id: devSource.initiatedByUserId,
|
|
97
|
+
replay_of_event_id: devSource.replayOfEventId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
export function toRunAttemptDto(attempt: WorkflowRunAttempt): WorkflowRunAttemptDto {
|
|
80
102
|
return {
|
|
81
103
|
id: attempt.id,
|
|
@@ -8,10 +8,15 @@ import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
|
|
|
8
8
|
import {createCapturingLogger} from '@shipfox/node-log/test';
|
|
9
9
|
import {eq} from 'drizzle-orm';
|
|
10
10
|
import type {StepStatus} from '#core/entities/step.js';
|
|
11
|
-
import {db} from '#db/db.js';
|
|
11
|
+
import {db, withTransaction} from '#db/db.js';
|
|
12
12
|
import {jobs} from '#db/schema/jobs.js';
|
|
13
13
|
import {steps as stepsTable} from '#db/schema/steps.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createWorkflowRun,
|
|
16
|
+
getJobsByWorkflowRunId,
|
|
17
|
+
getStepsByJobId,
|
|
18
|
+
insertRunningStepAttempt,
|
|
19
|
+
} from '#db/workflow-runs.js';
|
|
15
20
|
import {workflowModel} from '#test/factories/workflow-model.js';
|
|
16
21
|
import {insertRunningJobLease, mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
|
|
17
22
|
import {resolveTestAgentDefaults} from '#test/fixtures/agent-inter-module.js';
|
|
@@ -115,6 +120,13 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
|
115
120
|
thinking: 'xhigh',
|
|
116
121
|
credentials: {api_key: 'sk-workspace-secret'},
|
|
117
122
|
});
|
|
123
|
+
expect(resolveRuntimeCredentials).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
workspaceId: run.workspaceId,
|
|
126
|
+
runId: run.id,
|
|
127
|
+
stepAttemptId: expect.any(String),
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
118
130
|
});
|
|
119
131
|
|
|
120
132
|
test('derives the credential workspace from the leased job instead of hostile lease claims', async () => {
|
|
@@ -288,6 +300,63 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
|
288
300
|
expect(res.json().code).toBe('model-provider-not-configured');
|
|
289
301
|
});
|
|
290
302
|
|
|
303
|
+
test('returns managed provider policy details when workspace providers are disabled', async () => {
|
|
304
|
+
const {job, step} = await createRunningAgentStep();
|
|
305
|
+
resolveRuntimeCredentials.mockRejectedValueOnce(
|
|
306
|
+
createInterModuleKnownError(
|
|
307
|
+
agentInterModuleContract.methods.resolveRuntimeCredentials,
|
|
308
|
+
'workspace-providers-disabled',
|
|
309
|
+
{
|
|
310
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
311
|
+
managed_provider_id: 'shipfox',
|
|
312
|
+
},
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
316
|
+
|
|
317
|
+
const res = await app.inject({
|
|
318
|
+
method: 'GET',
|
|
319
|
+
url: runtimeConfigUrl(step.id, step.currentAttempt),
|
|
320
|
+
headers: {authorization: `Bearer ${token}`},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(res.statusCode).toBe(422);
|
|
324
|
+
expect(res.json()).toMatchObject({
|
|
325
|
+
code: 'workspace-providers-disabled',
|
|
326
|
+
details: {
|
|
327
|
+
message: 'This instance only supports provider `shipfox`.',
|
|
328
|
+
managed_provider_id: 'shipfox',
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('includes a fallback message when managed provider details omit one', async () => {
|
|
334
|
+
const {job, step} = await createRunningAgentStep();
|
|
335
|
+
resolveRuntimeCredentials.mockRejectedValueOnce(
|
|
336
|
+
createInterModuleKnownError(
|
|
337
|
+
agentInterModuleContract.methods.resolveRuntimeCredentials,
|
|
338
|
+
'workspace-providers-disabled',
|
|
339
|
+
{managed_provider_id: 'shipfox'},
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
343
|
+
|
|
344
|
+
const res = await app.inject({
|
|
345
|
+
method: 'GET',
|
|
346
|
+
url: runtimeConfigUrl(step.id, step.currentAttempt),
|
|
347
|
+
headers: {authorization: `Bearer ${token}`},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(res.statusCode).toBe(422);
|
|
351
|
+
expect(res.json()).toMatchObject({
|
|
352
|
+
code: 'workspace-providers-disabled',
|
|
353
|
+
details: {
|
|
354
|
+
message: 'Workspace provider configuration is disabled',
|
|
355
|
+
managed_provider_id: 'shipfox',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
291
360
|
test('returns 409 and reports when workspace credentials cannot be decrypted', async () => {
|
|
292
361
|
const {job, step} = await createRunningAgentStep();
|
|
293
362
|
resolveRuntimeCredentials.mockRejectedValueOnce(
|
|
@@ -420,6 +489,19 @@ async function createStep(params: {
|
|
|
420
489
|
const step = stepRows.find((candidate) => candidate.type === params.targetType);
|
|
421
490
|
if (!step) throw new Error(`createStep: ${params.targetType} step not found`);
|
|
422
491
|
await db().update(stepsTable).set({status: params.status}).where(eq(stepsTable.id, step.id));
|
|
492
|
+
if (params.status === 'running') {
|
|
493
|
+
await withTransaction((tx) =>
|
|
494
|
+
insertRunningStepAttempt(
|
|
495
|
+
{
|
|
496
|
+
jobExecutionId: step.jobExecutionId,
|
|
497
|
+
stepId: step.id,
|
|
498
|
+
attempt: step.currentAttempt,
|
|
499
|
+
config: step.config,
|
|
500
|
+
},
|
|
501
|
+
tx,
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
423
505
|
|
|
424
506
|
return {
|
|
425
507
|
run,
|
|
@@ -13,6 +13,7 @@ import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
|
13
13
|
import {captureException} from '@shipfox/node-error-monitoring';
|
|
14
14
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
15
15
|
import {ZodError} from 'zod';
|
|
16
|
+
import {getStepAttemptDetail} from '#db/index.js';
|
|
16
17
|
import {loadRunningLeasedStep} from './leased-step.js';
|
|
17
18
|
|
|
18
19
|
export function createAgentRuntimeConfigRoute(params: {
|
|
@@ -63,6 +64,22 @@ export function createAgentRuntimeConfigRoute(params: {
|
|
|
63
64
|
},
|
|
64
65
|
);
|
|
65
66
|
}
|
|
67
|
+
if (
|
|
68
|
+
isInterModuleKnownError(
|
|
69
|
+
agentInterModuleContract.methods.resolveRuntimeCredentials,
|
|
70
|
+
error,
|
|
71
|
+
) &&
|
|
72
|
+
error.code === 'workspace-providers-disabled'
|
|
73
|
+
) {
|
|
74
|
+
const message = error.details.message ?? 'Workspace provider configuration is disabled';
|
|
75
|
+
throw new ClientError(message, 'workspace-providers-disabled', {
|
|
76
|
+
status: 422,
|
|
77
|
+
details: {
|
|
78
|
+
managed_provider_id: error.details.managed_provider_id,
|
|
79
|
+
message,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
66
83
|
throw error;
|
|
67
84
|
},
|
|
68
85
|
handler: async (request, reply) => {
|
|
@@ -91,8 +108,15 @@ export function createAgentRuntimeConfigRoute(params: {
|
|
|
91
108
|
throw error;
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
const stepAttempt = await getStepAttemptDetail({stepId, attempt});
|
|
112
|
+
if (!stepAttempt) {
|
|
113
|
+
throw new ClientError('Step attempt not found', 'step-attempt-not-found', {status: 409});
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
const runtimeConfig = await params.agent.resolveRuntimeCredentials({
|
|
95
117
|
workspaceId,
|
|
118
|
+
runId: stepAttempt.workflowRunId,
|
|
119
|
+
stepAttemptId: stepAttempt.attempt.id,
|
|
96
120
|
harness: agentConfig.harness,
|
|
97
121
|
provider: agentConfig.provider,
|
|
98
122
|
model: agentConfig.model,
|