@shipfox/api-workflows 11.0.0 → 12.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 +8 -8
- package/CHANGELOG.md +100 -0
- package/README.md +75 -0
- package/dist/core/agent-defaults.d.ts +2 -1
- package/dist/core/agent-defaults.d.ts.map +1 -1
- package/dist/core/agent-defaults.js.map +1 -1
- package/dist/core/checkout.d.ts +7 -21
- package/dist/core/checkout.d.ts.map +1 -1
- package/dist/core/checkout.js +122 -41
- package/dist/core/checkout.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +8 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +13 -1
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job-listener-event.d.ts +2 -0
- package/dist/core/entities/job-listener-event.d.ts.map +1 -1
- package/dist/core/entities/job-listener-event.js.map +1 -1
- package/dist/core/entities/step.d.ts +13 -3
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +6 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts +1 -0
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run-attempt.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +17 -0
- 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 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +10 -5
- package/dist/core/errors.js.map +1 -1
- package/dist/core/job-execution.d.ts.map +1 -1
- package/dist/core/job-execution.js +14 -8
- package/dist/core/job-execution.js.map +1 -1
- package/dist/core/job-transition/decide-job-activation.d.ts +1 -0
- package/dist/core/job-transition/decide-job-activation.d.ts.map +1 -1
- package/dist/core/job-transition/decide-job-activation.js +1 -0
- package/dist/core/job-transition/decide-job-activation.js.map +1 -1
- package/dist/core/job-transition/derive-job-success.d.ts +1 -0
- package/dist/core/job-transition/derive-job-success.d.ts.map +1 -1
- package/dist/core/job-transition/derive-job-success.js +5 -8
- package/dist/core/job-transition/derive-job-success.js.map +1 -1
- package/dist/core/listener-execution-materialization.d.ts +6 -3
- package/dist/core/listener-execution-materialization.d.ts.map +1 -1
- package/dist/core/listener-execution-materialization.js +44 -6
- package/dist/core/listener-execution-materialization.js.map +1 -1
- package/dist/core/resolve-trigger-reference.d.ts +11 -0
- package/dist/core/resolve-trigger-reference.d.ts.map +1 -0
- package/dist/core/resolve-trigger-reference.js +39 -0
- package/dist/core/resolve-trigger-reference.js.map +1 -0
- package/dist/core/run-workflow.d.ts +1 -0
- package/dist/core/run-workflow.d.ts.map +1 -1
- package/dist/core/run-workflow.js +1 -0
- package/dist/core/run-workflow.js.map +1 -1
- package/dist/core/step-config/agent.d.ts +1 -2
- package/dist/core/step-config/agent.d.ts.map +1 -1
- package/dist/core/step-config/agent.js +22 -7
- package/dist/core/step-config/agent.js.map +1 -1
- package/dist/core/step-config/assemble-run-context.d.ts +19 -8
- package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
- package/dist/core/step-config/assemble-run-context.js +51 -19
- package/dist/core/step-config/assemble-run-context.js.map +1 -1
- package/dist/core/step-config/complete-step-dispatch-config.d.ts.map +1 -1
- package/dist/core/step-config/complete-step-dispatch-config.js +72 -4
- package/dist/core/step-config/complete-step-dispatch-config.js.map +1 -1
- package/dist/core/step-config/index.d.ts +1 -0
- package/dist/core/step-config/index.d.ts.map +1 -1
- package/dist/core/step-config/index.js +1 -0
- package/dist/core/step-config/index.js.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.d.ts +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.d.ts.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.js +49 -8
- package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
- package/dist/core/step-config/materialize-workflow-model.d.ts +3 -3
- package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
- package/dist/core/step-config/materialize-workflow-model.js +9 -5
- package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
- package/dist/core/step-config/resolve-job-execution-name.d.ts +3 -3
- package/dist/core/step-config/resolve-job-execution-name.d.ts.map +1 -1
- package/dist/core/step-config/resolve-job-execution-name.js +24 -13
- package/dist/core/step-config/resolve-job-execution-name.js.map +1 -1
- package/dist/core/step-config/resolve-step-config.d.ts.map +1 -1
- package/dist/core/step-config/resolve-step-config.js +174 -9
- package/dist/core/step-config/resolve-step-config.js.map +1 -1
- package/dist/core/step-config/resolve-workflow-run-name.d.ts +19 -0
- package/dist/core/step-config/resolve-workflow-run-name.d.ts.map +1 -0
- package/dist/core/step-config/resolve-workflow-run-name.js +73 -0
- package/dist/core/step-config/resolve-workflow-run-name.js.map +1 -0
- package/dist/core/step-transition/decide-step-transition.js.map +1 -1
- package/dist/core/step-transition/evaluate-gate.d.ts +2 -1
- package/dist/core/step-transition/evaluate-gate.d.ts.map +1 -1
- package/dist/core/step-transition/evaluate-gate.js +5 -3
- package/dist/core/step-transition/evaluate-gate.js.map +1 -1
- package/dist/core/workflow-run-creation.d.ts +3 -0
- package/dist/core/workflow-run-creation.d.ts.map +1 -1
- package/dist/core/workflow-run-creation.js +22 -7
- package/dist/core/workflow-run-creation.js.map +1 -1
- package/dist/db/db.d.ts +278 -10
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +3 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/job-listener-events.d.ts +2 -0
- package/dist/db/job-listener-events.d.ts.map +1 -1
- package/dist/db/job-listener-events.js +1 -0
- package/dist/db/job-listener-events.js.map +1 -1
- package/dist/db/job-listeners.d.ts +2 -0
- package/dist/db/job-listeners.d.ts.map +1 -1
- package/dist/db/job-listeners.js +81 -21
- package/dist/db/job-listeners.js.map +1 -1
- package/dist/db/schema/job-executions.d.ts +3 -3
- package/dist/db/schema/job-executions.d.ts.map +1 -1
- package/dist/db/schema/job-executions.js +8 -4
- package/dist/db/schema/job-executions.js.map +1 -1
- package/dist/db/schema/job-listener-events.d.ts +20 -0
- package/dist/db/schema/job-listener-events.d.ts.map +1 -1
- package/dist/db/schema/job-listener-events.js +2 -0
- package/dist/db/schema/job-listener-events.js.map +1 -1
- package/dist/db/schema/step-attempts.js +2 -2
- package/dist/db/schema/step-attempts.js.map +1 -1
- package/dist/db/schema/steps.d.ts +4 -2
- package/dist/db/schema/steps.d.ts.map +1 -1
- package/dist/db/schema/steps.js +1 -1
- package/dist/db/schema/steps.js.map +1 -1
- package/dist/db/schema/workflow-run-attempts.d.ts +21 -2
- package/dist/db/schema/workflow-run-attempts.d.ts.map +1 -1
- package/dist/db/schema/workflow-run-attempts.js +2 -0
- package/dist/db/schema/workflow-run-attempts.js.map +1 -1
- package/dist/db/schema/workflow-run-counters.d.ts +43 -0
- package/dist/db/schema/workflow-run-counters.d.ts.map +1 -0
- package/dist/db/schema/workflow-run-counters.js +10 -0
- package/dist/db/schema/workflow-run-counters.js.map +1 -0
- package/dist/db/schema/workflow-runs.d.ts +54 -1
- package/dist/db/schema/workflow-runs.d.ts.map +1 -1
- package/dist/db/schema/workflow-runs.js +10 -2
- package/dist/db/schema/workflow-runs.js.map +1 -1
- package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
- package/dist/db/workflow-runs/job-executions.js +35 -14
- package/dist/db/workflow-runs/job-executions.js.map +1 -1
- package/dist/db/workflow-runs/jobs.d.ts +2 -0
- package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
- package/dist/db/workflow-runs/jobs.js +9 -5
- package/dist/db/workflow-runs/jobs.js.map +1 -1
- package/dist/db/workflow-runs/queries.d.ts +51 -0
- package/dist/db/workflow-runs/queries.d.ts.map +1 -1
- package/dist/db/workflow-runs/queries.js +82 -2
- package/dist/db/workflow-runs/queries.js.map +1 -1
- package/dist/db/workflow-runs/run-create.d.ts +1 -0
- package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-create.js +159 -32
- package/dist/db/workflow-runs/run-create.js.map +1 -1
- package/dist/db/workflow-runs/run-rerun.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-rerun.js +13 -5
- package/dist/db/workflow-runs/run-rerun.js.map +1 -1
- package/dist/db/workflow-runs/shared.d.ts +1 -0
- package/dist/db/workflow-runs/shared.d.ts.map +1 -1
- package/dist/db/workflow-runs/shared.js +2 -1
- package/dist/db/workflow-runs/shared.js.map +1 -1
- package/dist/db/workflow-runs/steps.js +3 -3
- package/dist/db/workflow-runs/steps.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +4 -3
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +3 -2
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -0
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +9 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/dto/checkout-token.d.ts +1 -0
- package/dist/presentation/dto/checkout-token.d.ts.map +1 -1
- package/dist/presentation/dto/checkout-token.js +1 -0
- package/dist/presentation/dto/checkout-token.js.map +1 -1
- package/dist/presentation/dto/index.d.ts +1 -1
- package/dist/presentation/dto/index.d.ts.map +1 -1
- package/dist/presentation/dto/index.js +1 -1
- package/dist/presentation/dto/index.js.map +1 -1
- package/dist/presentation/dto/step.d.ts.map +1 -1
- package/dist/presentation/dto/step.js +2 -2
- package/dist/presentation/dto/step.js.map +1 -1
- package/dist/presentation/dto/workflow-run.d.ts +3 -1
- package/dist/presentation/dto/workflow-run.d.ts.map +1 -1
- package/dist/presentation/dto/workflow-run.js +34 -0
- package/dist/presentation/dto/workflow-run.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +31 -3
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/checkout-token.d.ts +1 -1
- package/dist/presentation/routes/checkout-token.d.ts.map +1 -1
- package/dist/presentation/routes/checkout-token.js +32 -20
- package/dist/presentation/routes/checkout-token.js.map +1 -1
- package/dist/presentation/routes/leased-step.d.ts +2 -0
- 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 +9 -3
- 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/presentation/routes/report-step.d.ts.map +1 -1
- package/dist/presentation/routes/report-step.js +9 -2
- package/dist/presentation/routes/report-step.js.map +1 -1
- package/dist/presentation/subscribers/on-job-steps-settled.js +1 -1
- package/dist/presentation/subscribers/on-job-steps-settled.js.map +1 -1
- package/dist/presentation/subscribers/on-runner-job-claimed.d.ts.map +1 -1
- package/dist/presentation/subscribers/on-runner-job-claimed.js +21 -0
- package/dist/presentation/subscribers/on-runner-job-claimed.js.map +1 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +2 -1
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/activities/orchestration-activities.d.ts +1 -0
- package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
- package/dist/temporal/activities/orchestration-activities.js.map +1 -1
- package/dist/temporal/constants.d.ts +1 -0
- package/dist/temporal/constants.d.ts.map +1 -1
- package/dist/temporal/constants.js +1 -0
- package/dist/temporal/constants.js.map +1 -1
- package/dist/temporal/index.d.ts +1 -1
- package/dist/temporal/index.d.ts.map +1 -1
- package/dist/temporal/index.js +1 -1
- package/dist/temporal/index.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +82 -43
- package/dist/temporal/workflows/index.d.ts +1 -1
- package/dist/temporal/workflows/index.d.ts.map +1 -1
- package/dist/temporal/workflows/index.js +1 -1
- package/dist/temporal/workflows/index.js.map +1 -1
- package/dist/temporal/workflows/job-execution-orchestration.d.ts +5 -0
- package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
- package/dist/temporal/workflows/job-execution-orchestration.js +77 -43
- package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
- package/dist/temporal/workflows/test-env.d.ts +5 -1
- package/dist/temporal/workflows/test-env.d.ts.map +1 -1
- package/dist/temporal/workflows/test-env.js +32 -11
- package/dist/temporal/workflows/test-env.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0000_initial.sql +14 -2
- package/drizzle/0001_windy_sunfire.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +93 -1
- package/drizzle/meta/0001_snapshot.json +1711 -0
- package/drizzle/meta/_journal.json +2 -2
- package/package.json +21 -21
- package/src/core/agent-defaults.ts +2 -1
- package/src/core/checkout.test.ts +378 -67
- package/src/core/checkout.ts +142 -59
- package/src/core/entities/job-execution.test.ts +21 -0
- package/src/core/entities/job-execution.ts +24 -0
- package/src/core/entities/job-listener-event.ts +3 -0
- package/src/core/entities/step.ts +15 -3
- package/src/core/entities/workflow-run-attempt.ts +1 -0
- package/src/core/entities/workflow-run.ts +16 -0
- package/src/core/errors.ts +21 -7
- package/src/core/job-execution.ts +27 -4
- package/src/core/job-transition/decide-job-activation.test.ts +4 -0
- package/src/core/job-transition/decide-job-activation.ts +2 -0
- package/src/core/job-transition/derive-job-success.test.ts +12 -0
- package/src/core/job-transition/derive-job-success.ts +6 -9
- package/src/core/listener-execution-materialization.test.ts +2 -0
- package/src/core/listener-execution-materialization.ts +57 -11
- package/src/core/resolve-trigger-reference.test.ts +160 -0
- package/src/core/resolve-trigger-reference.ts +53 -0
- package/src/core/run-workflow.ts +2 -0
- package/src/core/step-config/agent.ts +29 -12
- package/src/core/step-config/assemble-run-context.test.ts +149 -6
- package/src/core/step-config/assemble-run-context.ts +87 -25
- package/src/core/step-config/complete-step-dispatch-config.test.ts +125 -2
- package/src/core/step-config/complete-step-dispatch-config.ts +83 -1
- package/src/core/step-config/index.ts +7 -0
- package/src/core/step-config/materialize-job-execution-steps.test.ts +250 -1
- package/src/core/step-config/materialize-job-execution-steps.ts +57 -10
- package/src/core/step-config/materialize-workflow-model.test.ts +212 -4
- package/src/core/step-config/materialize-workflow-model.ts +33 -28
- package/src/core/step-config/resolve-job-execution-name.test.ts +37 -15
- package/src/core/step-config/resolve-job-execution-name.ts +30 -15
- package/src/core/step-config/resolve-step-config.ts +175 -10
- package/src/core/step-config/resolve-workflow-run-name.test.ts +115 -0
- package/src/core/step-config/resolve-workflow-run-name.ts +108 -0
- package/src/core/step-transition/decide-step-transition.ts +2 -2
- package/src/core/step-transition/evaluate-gate.test.ts +8 -0
- package/src/core/step-transition/evaluate-gate.ts +8 -1
- package/src/core/workflow-run-creation.test.ts +7 -4
- package/src/core/workflow-run-creation.ts +13 -7
- package/src/db/db.ts +2 -0
- package/src/db/index.ts +6 -0
- package/src/db/job-listener-events.test.ts +19 -0
- package/src/db/job-listener-events.ts +3 -0
- package/src/db/job-listeners.test.ts +187 -5
- package/src/db/job-listeners.ts +122 -30
- package/src/db/schema/job-executions.ts +12 -5
- package/src/db/schema/job-listener-events.ts +3 -0
- package/src/db/schema/step-attempts.ts +2 -2
- package/src/db/schema/steps.ts +2 -1
- package/src/db/schema/workflow-run-attempts.ts +2 -0
- package/src/db/schema/workflow-run-counters.ts +13 -0
- package/src/db/schema/workflow-runs.ts +11 -2
- package/src/db/workflow-runs/job-executions.ts +37 -15
- package/src/db/workflow-runs/jobs.ts +17 -4
- package/src/db/workflow-runs/queries.test.ts +206 -0
- package/src/db/workflow-runs/queries.ts +146 -2
- package/src/db/workflow-runs/run-create.test.ts +606 -8
- package/src/db/workflow-runs/run-create.ts +185 -40
- package/src/db/workflow-runs/run-rerun.test.ts +61 -2
- package/src/db/workflow-runs/run-rerun.ts +15 -5
- package/src/db/workflow-runs/shared.ts +2 -0
- package/src/db/workflow-runs/steps.ts +3 -3
- package/src/db/workflow-runs.ts +7 -0
- package/src/metrics/instance.ts +14 -0
- package/src/presentation/dto/checkout-token.test.ts +19 -9
- package/src/presentation/dto/checkout-token.ts +2 -1
- package/src/presentation/dto/index.ts +1 -1
- package/src/presentation/dto/job.test.ts +9 -0
- package/src/presentation/dto/step.test.ts +12 -0
- package/src/presentation/dto/step.ts +5 -2
- package/src/presentation/dto/workflow-run.ts +44 -2
- package/src/presentation/inter-module.test.ts +165 -0
- package/src/presentation/inter-module.ts +38 -4
- package/src/presentation/routes/checkout-token.test.ts +357 -142
- package/src/presentation/routes/checkout-token.ts +56 -26
- package/src/presentation/routes/get-run.test.ts +2 -0
- package/src/presentation/routes/get-step-secrets.test.ts +7 -2
- package/src/presentation/routes/leased-step.ts +9 -1
- package/src/presentation/routes/list-runs.test.ts +57 -0
- package/src/presentation/routes/list-runs.ts +8 -3
- package/src/presentation/routes/report-step.test.ts +25 -0
- package/src/presentation/routes/report-step.ts +10 -2
- package/src/presentation/subscribers/on-job-steps-settled.ts +1 -1
- package/src/presentation/subscribers/on-runner-job-claimed.test.ts +49 -0
- package/src/presentation/subscribers/on-runner-job-claimed.ts +22 -0
- package/src/temporal/activities/index.ts +1 -0
- package/src/temporal/activities/orchestration-activities.ts +1 -0
- package/src/temporal/constants.ts +1 -0
- package/src/temporal/index.ts +1 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/job-execution-orchestration.test.ts +142 -6
- package/src/temporal/workflows/job-execution-orchestration.ts +99 -50
- package/src/temporal/workflows/run-orchestration.test.ts +1 -1
- package/src/temporal/workflows/test-env.ts +50 -13
- package/test/factories/project.ts +2 -0
- package/test/factories/workflow-model.ts +80 -26
- package/test/factories/workflow-run.ts +4 -1
- package/test/fixtures/agent-inter-module.ts +2 -1
- package/test/fixtures/job-with-steps.ts +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/drizzle/0001_agent_tool_materialization.sql +0 -1
|
@@ -23,6 +23,7 @@ export const workflowRunAttempts = pgTable(
|
|
|
23
23
|
rerunMode: workflowRunRerunModeEnum('rerun_mode'),
|
|
24
24
|
rerunByUserId: uuid('rerun_by_user_id'),
|
|
25
25
|
model: jsonb('model').$type<WorkflowModel | WorkflowModelSnapshot>(),
|
|
26
|
+
vars: jsonb('vars').$type<Record<string, string>>(),
|
|
26
27
|
agentToolMaterialization: jsonb(
|
|
27
28
|
'agent_tool_materialization',
|
|
28
29
|
).$type<AgentToolMaterializationSnapshot>(),
|
|
@@ -53,6 +54,7 @@ export function toWorkflowRunAttempt(row: WorkflowRunAttemptDb): WorkflowRunAtte
|
|
|
53
54
|
rerunMode: row.rerunMode ?? null,
|
|
54
55
|
rerunByUserId: row.rerunByUserId,
|
|
55
56
|
model: row.model === null ? null : readPersistedWorkflowModel(row.model),
|
|
57
|
+
vars: row.vars ?? null,
|
|
56
58
|
agentToolMaterialization: row.agentToolMaterialization ?? null,
|
|
57
59
|
version: row.version,
|
|
58
60
|
createdAt: row.createdAt,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {integer, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
2
|
+
import {pgTable} from './common.js';
|
|
3
|
+
|
|
4
|
+
export const workflowRunCounters = pgTable(
|
|
5
|
+
'workflow_run_counters',
|
|
6
|
+
{
|
|
7
|
+
definitionId: uuid('definition_id').notNull(),
|
|
8
|
+
nextNumber: integer('next_number').notNull(),
|
|
9
|
+
},
|
|
10
|
+
(table) => [uniqueIndex('workflows_wrrc_definition_id_unique').on(table.definitionId)],
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export type WorkflowRunCounterDb = typeof workflowRunCounters.$inferSelect;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type {
|
|
17
17
|
TriggerPayload,
|
|
18
18
|
WorkflowRun,
|
|
19
|
+
WorkflowRunTriggerReference,
|
|
19
20
|
WorkflowSourceSnapshot,
|
|
20
21
|
} from '#core/entities/workflow-run.js';
|
|
21
22
|
import {pgTable} from './common.js';
|
|
@@ -37,13 +38,16 @@ export const workflowRuns = pgTable(
|
|
|
37
38
|
workspaceId: uuid('workspace_id').notNull(),
|
|
38
39
|
projectId: uuid('project_id').notNull(),
|
|
39
40
|
definitionId: uuid('definition_id').notNull(),
|
|
40
|
-
|
|
41
|
+
number: integer('number').notNull(),
|
|
42
|
+
name: text('name'),
|
|
43
|
+
workflowName: text('workflow_name').notNull(),
|
|
41
44
|
status: workflowRunStatusEnum('status').notNull().default('pending'),
|
|
42
45
|
currentAttempt: integer('current_attempt').notNull().default(1),
|
|
43
46
|
triggerProvider: text('trigger_provider'),
|
|
44
47
|
triggerSource: text('trigger_source').notNull(),
|
|
45
48
|
triggerEvent: text('trigger_event').notNull(),
|
|
46
49
|
triggerPayload: jsonb('trigger_payload').notNull().$type<TriggerPayload>(),
|
|
50
|
+
triggerReference: jsonb('trigger_reference').$type<WorkflowRunTriggerReference>(),
|
|
47
51
|
inputs: jsonb('inputs').$type<Record<string, unknown>>(),
|
|
48
52
|
sourceSnapshot: jsonb('source_snapshot').$type<WorkflowSourceSnapshot>(),
|
|
49
53
|
triggerIdempotencyKey: text('trigger_idempotency_key'),
|
|
@@ -56,6 +60,7 @@ export const workflowRuns = pgTable(
|
|
|
56
60
|
},
|
|
57
61
|
(table) => [
|
|
58
62
|
uniqueIndex('workflows_wr_trigger_idempotency_key_unique').on(table.triggerIdempotencyKey),
|
|
63
|
+
uniqueIndex('workflows_wr_definition_number_unique').on(table.definitionId, table.number),
|
|
59
64
|
index('workflows_wr_project_created_id_idx').on(table.projectId, table.createdAt, table.id),
|
|
60
65
|
index('workflows_wr_project_status_created_id_idx').on(
|
|
61
66
|
table.projectId,
|
|
@@ -92,13 +97,17 @@ export function toWorkflowRun(row: WorkflowRunDb): WorkflowRun {
|
|
|
92
97
|
workspaceId: row.workspaceId,
|
|
93
98
|
projectId: row.projectId,
|
|
94
99
|
definitionId: row.definitionId,
|
|
95
|
-
|
|
100
|
+
number: row.number,
|
|
101
|
+
name: row.name ?? row.workflowName,
|
|
102
|
+
workflowName: row.workflowName,
|
|
103
|
+
nameOverride: row.name,
|
|
96
104
|
status: row.status,
|
|
97
105
|
currentAttempt: row.currentAttempt,
|
|
98
106
|
triggerProvider: row.triggerProvider,
|
|
99
107
|
triggerSource: row.triggerSource,
|
|
100
108
|
triggerEvent: row.triggerEvent,
|
|
101
109
|
triggerPayload: row.triggerPayload as TriggerPayload,
|
|
110
|
+
triggerReference: row.triggerReference ?? null,
|
|
102
111
|
inputs: row.inputs ?? null,
|
|
103
112
|
sourceSnapshot: row.sourceSnapshot ?? null,
|
|
104
113
|
triggerIdempotencyKey: row.triggerIdempotencyKey,
|
|
@@ -32,35 +32,48 @@ import {
|
|
|
32
32
|
} from './shared.js';
|
|
33
33
|
import {bulkUpdateStepStatuses, getStepsByJobExecutionIdForUpdate} from './steps.js';
|
|
34
34
|
|
|
35
|
+
async function getJobExecutionFallbackName(tx: Tx, jobExecutionId: string): Promise<string> {
|
|
36
|
+
const [row] = await tx
|
|
37
|
+
.select({job: jobs})
|
|
38
|
+
.from(jobExecutions)
|
|
39
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
40
|
+
.where(eq(jobExecutions.id, jobExecutionId))
|
|
41
|
+
.limit(1);
|
|
42
|
+
if (!row) throw new JobNotFoundError(jobExecutionId);
|
|
43
|
+
return row.job.name ?? row.job.key;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export async function getJobExecutionById(id: string, tx?: Tx): Promise<JobExecution | undefined> {
|
|
36
47
|
const rows = await (tx ?? db())
|
|
37
|
-
.select()
|
|
48
|
+
.select({jobExecution: jobExecutions, job: jobs})
|
|
38
49
|
.from(jobExecutions)
|
|
50
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
39
51
|
.where(eq(jobExecutions.id, id))
|
|
40
52
|
.limit(1);
|
|
41
53
|
const row = rows[0];
|
|
42
|
-
return row ? toJobExecution(row) : undefined;
|
|
54
|
+
return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export async function getJobExecutionsByWorkflowRunAttemptId(
|
|
46
58
|
workflowRunAttemptId: string,
|
|
47
59
|
): Promise<JobExecution[]> {
|
|
48
60
|
const rows = await db()
|
|
49
|
-
.select({jobExecution: jobExecutions})
|
|
61
|
+
.select({jobExecution: jobExecutions, job: jobs})
|
|
50
62
|
.from(jobExecutions)
|
|
51
63
|
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
52
64
|
.where(eq(jobs.workflowRunAttemptId, workflowRunAttemptId))
|
|
53
65
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
54
|
-
return rows.map((row) => toJobExecution(row.jobExecution));
|
|
66
|
+
return rows.map((row) => toJobExecution(row.jobExecution, row.job.name ?? row.job.key));
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
export async function getJobExecutionsByJobId(jobId: string): Promise<JobExecution[]> {
|
|
58
70
|
const rows = await db()
|
|
59
|
-
.select()
|
|
71
|
+
.select({jobExecution: jobExecutions, job: jobs})
|
|
60
72
|
.from(jobExecutions)
|
|
73
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
61
74
|
.where(eq(jobExecutions.jobId, jobId))
|
|
62
75
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
63
|
-
return rows.map(toJobExecution);
|
|
76
|
+
return rows.map((row) => toJobExecution(row.jobExecution, row.job.name ?? row.job.key));
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
export async function getFirstJobExecutionByJobId(
|
|
@@ -68,13 +81,14 @@ export async function getFirstJobExecutionByJobId(
|
|
|
68
81
|
tx?: Tx,
|
|
69
82
|
): Promise<JobExecution | undefined> {
|
|
70
83
|
const rows = await (tx ?? db())
|
|
71
|
-
.select()
|
|
84
|
+
.select({jobExecution: jobExecutions, job: jobs})
|
|
72
85
|
.from(jobExecutions)
|
|
86
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
73
87
|
.where(eq(jobExecutions.jobId, jobId))
|
|
74
88
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id))
|
|
75
89
|
.limit(1);
|
|
76
90
|
const row = rows[0];
|
|
77
|
-
return row ? toJobExecution(row) : undefined;
|
|
91
|
+
return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
export async function getLatestJobExecutionByJobId(
|
|
@@ -82,13 +96,14 @@ export async function getLatestJobExecutionByJobId(
|
|
|
82
96
|
tx?: Tx,
|
|
83
97
|
): Promise<JobExecution | undefined> {
|
|
84
98
|
const rows = await (tx ?? db())
|
|
85
|
-
.select()
|
|
99
|
+
.select({jobExecution: jobExecutions, job: jobs})
|
|
86
100
|
.from(jobExecutions)
|
|
101
|
+
.innerJoin(jobs, eq(jobExecutions.jobId, jobs.id))
|
|
87
102
|
.where(eq(jobExecutions.jobId, jobId))
|
|
88
103
|
.orderBy(desc(jobExecutions.sequence), desc(jobExecutions.id))
|
|
89
104
|
.limit(1);
|
|
90
105
|
const row = rows[0];
|
|
91
|
-
return row ? toJobExecution(row) : undefined;
|
|
106
|
+
return row ? toJobExecution(row.jobExecution, row.job.name ?? row.job.key) : undefined;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
export interface UpdateJobExecutionStatusAtVersionParams {
|
|
@@ -129,10 +144,14 @@ async function resolveJobExecutionOutputs(
|
|
|
129
144
|
.from(jobExecutions)
|
|
130
145
|
.where(eq(jobExecutions.jobId, target.job.id))
|
|
131
146
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
147
|
+
const fallbackName = target.job.name ?? target.job.key;
|
|
132
148
|
const executions = executionRows.map((row) =>
|
|
133
149
|
row.id === target.execution.id
|
|
134
|
-
? toJobExecution(
|
|
135
|
-
|
|
150
|
+
? toJobExecution(
|
|
151
|
+
{...row, status: params.status, statusReason: params.statusReason},
|
|
152
|
+
fallbackName,
|
|
153
|
+
)
|
|
154
|
+
: toJobExecution(row, fallbackName),
|
|
136
155
|
);
|
|
137
156
|
const jobExecution = executions.find((execution) => execution.id === target.execution.id);
|
|
138
157
|
if (!jobExecution) throw new JobNotFoundError(params.jobExecutionId);
|
|
@@ -217,7 +236,10 @@ async function updateJobExecutionStatusAtVersion(
|
|
|
217
236
|
|
|
218
237
|
const row = rows[0];
|
|
219
238
|
if (!row) return null;
|
|
220
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
execution: toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)),
|
|
241
|
+
changed: true,
|
|
242
|
+
};
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
export interface UpdateJobExecutionStatusParams {
|
|
@@ -250,7 +272,7 @@ export async function updateJobExecutionStatus(
|
|
|
250
272
|
.where(eq(jobExecutions.id, params.jobExecutionId))
|
|
251
273
|
.limit(1)
|
|
252
274
|
)[0];
|
|
253
|
-
return row ? toJobExecution(row) : undefined;
|
|
275
|
+
return row ? toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)) : undefined;
|
|
254
276
|
},
|
|
255
277
|
matchFn: (execution) =>
|
|
256
278
|
(execution.status === params.status && execution.statusReason === statusReason) ||
|
|
@@ -317,7 +339,7 @@ export async function failJobExecutionAsTimedOut(params: {
|
|
|
317
339
|
.where(eq(jobExecutions.id, params.jobExecutionId))
|
|
318
340
|
.limit(1)
|
|
319
341
|
)[0];
|
|
320
|
-
return row ? toJobExecution(row) : undefined;
|
|
342
|
+
return row ? toJobExecution(row, await getJobExecutionFallbackName(tx, row.id)) : undefined;
|
|
321
343
|
},
|
|
322
344
|
matchFn: (execution) => (execution.timedOutAt !== null ? {execution, changed: false} : null),
|
|
323
345
|
failureMessage: `Optimistic lock failure: job execution ${params.jobExecutionId} version ${params.expectedVersion}`,
|
|
@@ -4,6 +4,7 @@ import {and, asc, desc, eq, inArray, notInArray, sql} from 'drizzle-orm';
|
|
|
4
4
|
import {isJobTerminal, type Job, type JobStatus, type JobStatusReason} from '#core/entities/job.js';
|
|
5
5
|
import type {JobExecution} from '#core/entities/job-execution.js';
|
|
6
6
|
import type {PersistedEvaluationTraceEntry} from '#core/entities/step.js';
|
|
7
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
7
8
|
import {JobNotFoundError} from '#core/errors.js';
|
|
8
9
|
import {
|
|
9
10
|
type DeriveJobSuccessResult,
|
|
@@ -58,11 +59,16 @@ export async function getJobById(id: string): Promise<Job | undefined> {
|
|
|
58
59
|
export interface JobScope {
|
|
59
60
|
workspaceId: string;
|
|
60
61
|
projectId: string;
|
|
62
|
+
triggerReference: WorkflowRunTriggerReference | null;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export async function getJobScope(jobId: string): Promise<JobScope | undefined> {
|
|
64
66
|
const rows = await db()
|
|
65
|
-
.select({
|
|
67
|
+
.select({
|
|
68
|
+
workspaceId: workflowRuns.workspaceId,
|
|
69
|
+
projectId: workflowRuns.projectId,
|
|
70
|
+
triggerReference: workflowRuns.triggerReference,
|
|
71
|
+
})
|
|
66
72
|
.from(jobs)
|
|
67
73
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
68
74
|
.innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
|
|
@@ -98,7 +104,8 @@ export async function getDirectDependencyJobContexts(
|
|
|
98
104
|
context = {job: toJob(row.job), executions: []};
|
|
99
105
|
contextsByJobId.set(row.job.id, context);
|
|
100
106
|
}
|
|
101
|
-
if (row.execution)
|
|
107
|
+
if (row.execution)
|
|
108
|
+
context.executions.push(toJobExecution(row.execution, row.job.name ?? row.job.key));
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
return [...contextsByJobId.values()];
|
|
@@ -169,6 +176,7 @@ export async function evaluateJobActivations(
|
|
|
169
176
|
run: toWorkflowRun(target.run),
|
|
170
177
|
job,
|
|
171
178
|
condition: modelJob?.if,
|
|
179
|
+
vars: target.attempt.vars ?? undefined,
|
|
172
180
|
dependencies: job.dependencies.flatMap((key) => {
|
|
173
181
|
const dependency = contextsByJobKey.get(key);
|
|
174
182
|
return dependency === undefined ? [] : [dependency];
|
|
@@ -249,7 +257,8 @@ async function directDependencyContextsByJobKey(
|
|
|
249
257
|
context = {job: toJob(row.job), executions: []};
|
|
250
258
|
contextsByJobKey.set(row.job.key, context);
|
|
251
259
|
}
|
|
252
|
-
if (row.execution)
|
|
260
|
+
if (row.execution)
|
|
261
|
+
context.executions.push(toJobExecution(row.execution, row.job.name ?? row.job.key));
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
return contextsByJobKey;
|
|
@@ -383,6 +392,7 @@ export async function resolveJobStatusFromJobExecutions(params: {
|
|
|
383
392
|
.from(jobExecutions)
|
|
384
393
|
.where(eq(jobExecutions.jobId, params.jobId))
|
|
385
394
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
395
|
+
const workflowContext = await getWorkflowContextForJob(params.jobId, tx);
|
|
386
396
|
|
|
387
397
|
if (jobExecutionRows.length === 0) {
|
|
388
398
|
throw new Error(`Cannot resolve job ${params.jobId}: no job executions found`);
|
|
@@ -390,8 +400,11 @@ export async function resolveJobStatusFromJobExecutions(params: {
|
|
|
390
400
|
|
|
391
401
|
const {status, statusReason, trace} = evaluateJobSuccess({
|
|
392
402
|
success: jobRow.success,
|
|
393
|
-
executions: jobExecutionRows.map(
|
|
403
|
+
executions: jobExecutionRows.map((execution) =>
|
|
404
|
+
toJobExecution(execution, jobRow.name ?? jobRow.key),
|
|
405
|
+
),
|
|
394
406
|
jobs: await getDirectDependencyJobContexts(params.jobId, tx),
|
|
407
|
+
vars: workflowContext.vars ?? undefined,
|
|
395
408
|
});
|
|
396
409
|
|
|
397
410
|
return optimisticLockRetry({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {WORKFLOW_RUN_JOB_PREVIEW_LIMIT} from '@shipfox/api-workflows-dto';
|
|
1
2
|
import {buildModel, createTestRun} from '#test/helpers/workflow-runs.js';
|
|
2
3
|
import {
|
|
3
4
|
createRerunWorkflowRun,
|
|
@@ -8,8 +9,10 @@ import {
|
|
|
8
9
|
getWorkflowJobExecutionDepth,
|
|
9
10
|
getWorkflowRunById,
|
|
10
11
|
listRunAttempts,
|
|
12
|
+
listWorkflowRunJobSummaries,
|
|
11
13
|
listWorkflowRunsByProject,
|
|
12
14
|
updateJobExecutionStatus,
|
|
15
|
+
updateJobStatus,
|
|
13
16
|
updateWorkflowRunStatus,
|
|
14
17
|
} from '../workflow-runs.js';
|
|
15
18
|
|
|
@@ -136,6 +139,178 @@ describe('workflow run queries', () => {
|
|
|
136
139
|
});
|
|
137
140
|
});
|
|
138
141
|
|
|
142
|
+
describe('listWorkflowRunJobSummaries', () => {
|
|
143
|
+
test('returns jobs in graph order keyed by run, for many runs at once', async () => {
|
|
144
|
+
const model = buildModel({
|
|
145
|
+
jobs: {
|
|
146
|
+
build: {steps: [{run: 'echo build'}]},
|
|
147
|
+
test: {needs: 'build', steps: [{run: 'echo test'}]},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const first = await createWorkflowRun({
|
|
151
|
+
workspaceId,
|
|
152
|
+
projectId,
|
|
153
|
+
definitionId,
|
|
154
|
+
model,
|
|
155
|
+
triggerPayload: manualTrigger(),
|
|
156
|
+
});
|
|
157
|
+
const second = await createWorkflowRun({
|
|
158
|
+
workspaceId,
|
|
159
|
+
projectId,
|
|
160
|
+
definitionId,
|
|
161
|
+
model,
|
|
162
|
+
triggerPayload: manualTrigger(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const summaries = await listWorkflowRunJobSummaries([
|
|
166
|
+
{id: first.id, currentAttempt: first.currentAttempt},
|
|
167
|
+
{id: second.id, currentAttempt: second.currentAttempt},
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(summaries.get(first.id)?.preview.map((job) => job.key)).toEqual(['build', 'test']);
|
|
171
|
+
expect(summaries.get(second.id)?.preview.map((job) => job.key)).toEqual(['build', 'test']);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('scopes jobs to the current attempt so a re-run does not stack both attempts', async () => {
|
|
175
|
+
const source = await createWorkflowRun({
|
|
176
|
+
workspaceId,
|
|
177
|
+
projectId,
|
|
178
|
+
definitionId,
|
|
179
|
+
model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
|
|
180
|
+
triggerPayload: manualTrigger(),
|
|
181
|
+
});
|
|
182
|
+
const [firstAttemptJob] = await getJobsByWorkflowRunId(source.id);
|
|
183
|
+
await updateWorkflowRunStatus({
|
|
184
|
+
workflowRunId: source.id,
|
|
185
|
+
status: 'failed',
|
|
186
|
+
expectedVersion: 1,
|
|
187
|
+
});
|
|
188
|
+
const rerun = await createRerunWorkflowRun({
|
|
189
|
+
workflowRunId: source.id,
|
|
190
|
+
mode: 'all',
|
|
191
|
+
actorUserId: crypto.randomUUID(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const summaries = await listWorkflowRunJobSummaries([
|
|
195
|
+
{id: source.id, currentAttempt: rerun.currentAttempt},
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
expect(rerun.currentAttempt).toBe(2);
|
|
199
|
+
expect(summaries.get(source.id)?.preview.map((job) => job.key)).toEqual(['build']);
|
|
200
|
+
expect(summaries.get(source.id)?.preview[0]?.id).not.toBe(firstAttemptJob?.id);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// The caller pins the attempt, so a re-run landing after its read still yields the jobs
|
|
204
|
+
// that belong to the metadata it is about to render, not whichever attempt is newest.
|
|
205
|
+
test('returns the caller requested attempt, not whichever is current now', async () => {
|
|
206
|
+
const source = await createWorkflowRun({
|
|
207
|
+
workspaceId,
|
|
208
|
+
projectId,
|
|
209
|
+
definitionId,
|
|
210
|
+
model: buildModel({jobs: {build: {steps: [{run: 'echo build'}]}}}),
|
|
211
|
+
triggerPayload: manualTrigger(),
|
|
212
|
+
});
|
|
213
|
+
const [firstAttemptJob] = await getJobsByWorkflowRunId(source.id);
|
|
214
|
+
await updateWorkflowRunStatus({
|
|
215
|
+
workflowRunId: source.id,
|
|
216
|
+
status: 'failed',
|
|
217
|
+
expectedVersion: 1,
|
|
218
|
+
});
|
|
219
|
+
await createRerunWorkflowRun({
|
|
220
|
+
workflowRunId: source.id,
|
|
221
|
+
mode: 'all',
|
|
222
|
+
actorUserId: crypto.randomUUID(),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const summaries = await listWorkflowRunJobSummaries([{id: source.id, currentAttempt: 1}]);
|
|
226
|
+
|
|
227
|
+
expect(summaries.get(source.id)?.preview[0]?.id).toBe(firstAttemptJob?.id);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// The row draws a bounded strip, so the query must not hand back one row per job of a
|
|
231
|
+
// workflow that has no job limit; everything past the preview is described by counts.
|
|
232
|
+
test('caps the preview and still counts every job past it', async () => {
|
|
233
|
+
const jobCount = WORKFLOW_RUN_JOB_PREVIEW_LIMIT + 9;
|
|
234
|
+
const run = await createWorkflowRun({
|
|
235
|
+
workspaceId,
|
|
236
|
+
projectId,
|
|
237
|
+
definitionId,
|
|
238
|
+
model: buildModel({
|
|
239
|
+
jobs: Object.fromEntries(
|
|
240
|
+
Array.from({length: jobCount}, (_, index) => [
|
|
241
|
+
`job-${String(index).padStart(2, '0')}`,
|
|
242
|
+
{steps: [{run: `echo ${index}`}]},
|
|
243
|
+
]),
|
|
244
|
+
),
|
|
245
|
+
}),
|
|
246
|
+
triggerPayload: manualTrigger(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
250
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
expect(summary.get(run.id)?.preview).toHaveLength(WORKFLOW_RUN_JOB_PREVIEW_LIMIT);
|
|
254
|
+
expect(totalOf(summary.get(run.id))).toBe(jobCount);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('counts each status the run holds, not only the previewed ones', async () => {
|
|
258
|
+
const run = await createWorkflowRun({
|
|
259
|
+
workspaceId,
|
|
260
|
+
projectId,
|
|
261
|
+
definitionId,
|
|
262
|
+
model: buildModel({
|
|
263
|
+
jobs: {build: {steps: [{run: 'echo build'}]}, test: {steps: [{run: 'echo test'}]}},
|
|
264
|
+
}),
|
|
265
|
+
triggerPayload: manualTrigger(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const summary = await listWorkflowRunJobSummaries([
|
|
269
|
+
{id: run.id, currentAttempt: run.currentAttempt},
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
expect(summary.get(run.id)?.statusCounts).toEqual([{status: 'pending', count: 2}]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Checks the invariant the snapshot exists to protect: for a run inside the preview
|
|
276
|
+
// bound, the statuses drawn and the statuses counted describe the same jobs and must
|
|
277
|
+
// agree exactly. The read races a commit to give the anomaly a chance to appear, so this
|
|
278
|
+
// samples rather than proves; the guarantee itself comes from the isolation level, which
|
|
279
|
+
// no assertion here can force an interleaving against.
|
|
280
|
+
test('agrees between preview and counts while a job settles underneath', async () => {
|
|
281
|
+
const run = await createWorkflowRun({
|
|
282
|
+
workspaceId,
|
|
283
|
+
projectId,
|
|
284
|
+
definitionId,
|
|
285
|
+
model: buildModel({
|
|
286
|
+
jobs: {build: {steps: [{run: 'echo build'}]}, test: {steps: [{run: 'echo test'}]}},
|
|
287
|
+
}),
|
|
288
|
+
triggerPayload: manualTrigger(),
|
|
289
|
+
});
|
|
290
|
+
const [firstJob] = await getJobsByWorkflowRunId(run.id);
|
|
291
|
+
if (!firstJob) throw new Error('expected the run to have jobs');
|
|
292
|
+
|
|
293
|
+
// Commits a status change while the read is in flight. A snapshot taken per statement
|
|
294
|
+
// would let one half of the answer see it and the other half miss it.
|
|
295
|
+
const settleMidRead = updateJobStatus({
|
|
296
|
+
jobId: firstJob.id,
|
|
297
|
+
status: 'failed',
|
|
298
|
+
expectedVersion: firstJob.version,
|
|
299
|
+
});
|
|
300
|
+
const [summaries] = await Promise.all([
|
|
301
|
+
listWorkflowRunJobSummaries([{id: run.id, currentAttempt: run.currentAttempt}]),
|
|
302
|
+
settleMidRead,
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
const summary = summaries.get(run.id);
|
|
306
|
+
expect(previewCounts(summary)).toEqual(statusCountMap(summary));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('returns an empty map without querying for an empty page', async () => {
|
|
310
|
+
await expect(listWorkflowRunJobSummaries([])).resolves.toEqual(new Map());
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
139
314
|
describe('listWorkflowRunsByProject', () => {
|
|
140
315
|
test('returns runs ordered by creation descending', async () => {
|
|
141
316
|
await createWorkflowRun({
|
|
@@ -254,3 +429,34 @@ describe('workflow run queries', () => {
|
|
|
254
429
|
});
|
|
255
430
|
});
|
|
256
431
|
});
|
|
432
|
+
|
|
433
|
+
function manualTrigger() {
|
|
434
|
+
return {
|
|
435
|
+
source: 'manual',
|
|
436
|
+
event: 'fire',
|
|
437
|
+
subscriptionId: crypto.randomUUID(),
|
|
438
|
+
userId: crypto.randomUUID(),
|
|
439
|
+
} as const;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function totalOf(summary: {statusCounts: Array<{count: number}>} | undefined): number {
|
|
443
|
+
return (summary?.statusCounts ?? []).reduce((total, entry) => total + entry.count, 0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Statuses the preview actually drew, counted. */
|
|
447
|
+
function previewCounts(
|
|
448
|
+
summary: {preview: Array<{status: string}>} | undefined,
|
|
449
|
+
): Record<string, number> {
|
|
450
|
+
const counts: Record<string, number> = {};
|
|
451
|
+
for (const job of summary?.preview ?? []) counts[job.status] = (counts[job.status] ?? 0) + 1;
|
|
452
|
+
return counts;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The same shape read off the totals, so the two halves can be compared directly. */
|
|
456
|
+
function statusCountMap(
|
|
457
|
+
summary: {statusCounts: Array<{status: string; count: number}>} | undefined,
|
|
458
|
+
): Record<string, number> {
|
|
459
|
+
return Object.fromEntries(
|
|
460
|
+
(summary?.statusCounts ?? []).map(({status, count}) => [status, count]),
|
|
461
|
+
);
|
|
462
|
+
}
|