@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {WORKFLOWS_JOB_EVENT_DELIVERED} from '@shipfox/api-workflows-dto';
|
|
2
2
|
import {eq} from 'drizzle-orm';
|
|
3
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
3
4
|
import {db} from '#db/db.js';
|
|
4
5
|
import {jobListenerEvents} from '#db/schema/job-listener-events.js';
|
|
5
6
|
import {jobs} from '#db/schema/jobs.js';
|
|
@@ -13,6 +14,7 @@ interface DeliverOverrides {
|
|
|
13
14
|
disposition?: 'fire' | 'resolve';
|
|
14
15
|
eventRef?: string;
|
|
15
16
|
provider?: string;
|
|
17
|
+
triggerReference?: WorkflowRunTriggerReference | null;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function deliver(overrides: DeliverOverrides = {}) {
|
|
@@ -24,6 +26,7 @@ function deliver(overrides: DeliverOverrides = {}) {
|
|
|
24
26
|
source: 'github',
|
|
25
27
|
event: 'pull_request_review',
|
|
26
28
|
provider: overrides.provider ?? 'github',
|
|
29
|
+
triggerReference: overrides.triggerReference,
|
|
27
30
|
payload: {action: 'submitted'},
|
|
28
31
|
receivedAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
29
32
|
});
|
|
@@ -56,6 +59,22 @@ describe('deliverEventToListener', () => {
|
|
|
56
59
|
expect(rows[0]?.disposition).toBe(disposition);
|
|
57
60
|
});
|
|
58
61
|
|
|
62
|
+
it('persists the normalized trigger reference with the listener event', async () => {
|
|
63
|
+
const job = await jobFactory.create({}, {transient: {status: 'pending'}});
|
|
64
|
+
const triggerReference: WorkflowRunTriggerReference = {
|
|
65
|
+
project: {id: crypto.randomUUID()},
|
|
66
|
+
repository: 'acme/api',
|
|
67
|
+
ref: 'refs/heads/main',
|
|
68
|
+
commit: 'a'.repeat(40),
|
|
69
|
+
actor: 'octocat',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await deliver({jobId: job.id, triggerReference});
|
|
73
|
+
|
|
74
|
+
const rows = await listEvents(job.id);
|
|
75
|
+
expect(rows[0]?.triggerReference).toEqual(triggerReference);
|
|
76
|
+
});
|
|
77
|
+
|
|
59
78
|
it('skips terminal jobs without throwing or buffering', async () => {
|
|
60
79
|
const job = await jobFactory.create({}, {transient: {status: 'succeeded'}});
|
|
61
80
|
|
|
@@ -2,6 +2,7 @@ import {WORKFLOWS_JOB_EVENT_DELIVERED} from '@shipfox/api-workflows-dto';
|
|
|
2
2
|
import {eq} from 'drizzle-orm';
|
|
3
3
|
import {isJobTerminal} from '#core/entities/job.js';
|
|
4
4
|
import type {JobListenerEventDisposition} from '#core/entities/job-listener-event.js';
|
|
5
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
5
6
|
import {recordListenerEventReceived} from '#metrics/instance.js';
|
|
6
7
|
import {db} from './db.js';
|
|
7
8
|
import {writeWorkflowsOutboxEvent} from './outbox-writes.js';
|
|
@@ -16,6 +17,7 @@ export interface DeliverEventToListenerParams {
|
|
|
16
17
|
source: string;
|
|
17
18
|
event: string;
|
|
18
19
|
provider: string;
|
|
20
|
+
triggerReference?: WorkflowRunTriggerReference | null | undefined;
|
|
19
21
|
payload: unknown;
|
|
20
22
|
receivedAt: Date;
|
|
21
23
|
}
|
|
@@ -47,6 +49,7 @@ export async function deliverEventToListener(
|
|
|
47
49
|
deliveryId: params.deliveryId,
|
|
48
50
|
source: params.source,
|
|
49
51
|
event: params.event,
|
|
52
|
+
triggerReference: params.triggerReference ?? null,
|
|
50
53
|
payload: params.payload,
|
|
51
54
|
receivedAt: params.receivedAt,
|
|
52
55
|
})
|
|
@@ -6,6 +6,7 @@ import {createWorkflowExpression} from '@shipfox/expression';
|
|
|
6
6
|
import {and, asc, eq, isNull} from 'drizzle-orm';
|
|
7
7
|
import type {JobListeningTrigger, JobStatus} from '#core/entities/job.js';
|
|
8
8
|
import type {JobExecutionStatus} from '#core/entities/job-execution.js';
|
|
9
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
9
10
|
import {nextStepForJob, recordStepResult} from '#core/job-execution.js';
|
|
10
11
|
import {db} from '#db/db.js';
|
|
11
12
|
import {deliverEventToListener} from '#db/job-listener-events.js';
|
|
@@ -83,6 +84,7 @@ function bufferEvent(
|
|
|
83
84
|
disposition: 'fire' | 'resolve' = 'fire',
|
|
84
85
|
eventRef = crypto.randomUUID(),
|
|
85
86
|
receivedAt = new Date('2026-01-01T00:00:00.000Z'),
|
|
87
|
+
triggerReference?: WorkflowRunTriggerReference | null,
|
|
86
88
|
) {
|
|
87
89
|
return deliverEventToListener({
|
|
88
90
|
jobId,
|
|
@@ -92,6 +94,7 @@ function bufferEvent(
|
|
|
92
94
|
source: 'github',
|
|
93
95
|
event: 'pull_request',
|
|
94
96
|
provider: 'github',
|
|
97
|
+
triggerReference,
|
|
95
98
|
payload: {action: 'opened'},
|
|
96
99
|
receivedAt,
|
|
97
100
|
});
|
|
@@ -324,9 +327,16 @@ describe('activateJobListener', () => {
|
|
|
324
327
|
const snapshot = payload.on[0]?.filter_snapshot;
|
|
325
328
|
expect(snapshot).toEqual({
|
|
326
329
|
run: expect.objectContaining({id: expect.any(String), name: 'Test Workflow'}),
|
|
327
|
-
trigger: {
|
|
330
|
+
trigger: {
|
|
331
|
+
source: 'github',
|
|
332
|
+
event: 'pull_request',
|
|
333
|
+
project: null,
|
|
334
|
+
repository: null,
|
|
335
|
+
ref: null,
|
|
336
|
+
commit: null,
|
|
337
|
+
},
|
|
328
338
|
inputs: {environment: 'prod'},
|
|
329
|
-
job: {key: 'await'},
|
|
339
|
+
job: {key: 'await', name: 'await'},
|
|
330
340
|
jobs: {
|
|
331
341
|
build: expect.objectContaining({
|
|
332
342
|
status: 'succeeded',
|
|
@@ -473,6 +483,69 @@ describe('drainListenerEventsIntoExecution', () => {
|
|
|
473
483
|
]);
|
|
474
484
|
});
|
|
475
485
|
|
|
486
|
+
it('exposes each buffered event reference on its execution event', async () => {
|
|
487
|
+
const job = await createListeningJob({status: 'running', listenerStatus: 'listening'});
|
|
488
|
+
const firstReference: WorkflowRunTriggerReference = {
|
|
489
|
+
project: {id: crypto.randomUUID()},
|
|
490
|
+
repository: 'acme/one',
|
|
491
|
+
ref: 'refs/heads/main',
|
|
492
|
+
commit: '1'.repeat(40),
|
|
493
|
+
actor: 'octocat',
|
|
494
|
+
};
|
|
495
|
+
const secondReference: WorkflowRunTriggerReference = {
|
|
496
|
+
project: {id: crypto.randomUUID()},
|
|
497
|
+
repository: 'acme/two',
|
|
498
|
+
ref: 'refs/pull/12/head',
|
|
499
|
+
commit: '2'.repeat(40),
|
|
500
|
+
actor: 'octocat',
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
await bufferEvent(
|
|
504
|
+
job.id,
|
|
505
|
+
'fire',
|
|
506
|
+
crypto.randomUUID(),
|
|
507
|
+
new Date('2026-01-01T00:00:00.000Z'),
|
|
508
|
+
firstReference,
|
|
509
|
+
);
|
|
510
|
+
await bufferEvent(
|
|
511
|
+
job.id,
|
|
512
|
+
'fire',
|
|
513
|
+
crypto.randomUUID(),
|
|
514
|
+
new Date('2026-01-01T00:01:00.000Z'),
|
|
515
|
+
secondReference,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
await drainListenerEventsIntoExecution({jobId: job.id, expectedSequence: 1});
|
|
519
|
+
|
|
520
|
+
const [execution] = await db()
|
|
521
|
+
.select()
|
|
522
|
+
.from(jobExecutions)
|
|
523
|
+
.where(and(eq(jobExecutions.jobId, job.id), eq(jobExecutions.sequence, 1)));
|
|
524
|
+
// The execution event carries the reference's location fields, not its actor:
|
|
525
|
+
// `WorkflowExecutionEvent` enumerates its own shape and the trigger actor is not part of
|
|
526
|
+
// it. Widening that is the listening-jobs contract's call, not this branch's.
|
|
527
|
+
const {actor: _firstActor, ...firstEventFields} = firstReference;
|
|
528
|
+
const {actor: _secondActor, ...secondEventFields} = secondReference;
|
|
529
|
+
expect(execution?.triggerEvents).toEqual([
|
|
530
|
+
{
|
|
531
|
+
source: 'github',
|
|
532
|
+
event: 'pull_request',
|
|
533
|
+
delivery_id: expect.any(String),
|
|
534
|
+
received_at: '2026-01-01T00:00:00.000Z',
|
|
535
|
+
...firstEventFields,
|
|
536
|
+
data: {action: 'opened'},
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
source: 'github',
|
|
540
|
+
event: 'pull_request',
|
|
541
|
+
delivery_id: expect.any(String),
|
|
542
|
+
received_at: '2026-01-01T00:01:00.000Z',
|
|
543
|
+
...secondEventFields,
|
|
544
|
+
data: {action: 'opened'},
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
547
|
+
});
|
|
548
|
+
|
|
476
549
|
it('materializes a pending execution from buffered fire events and consumes them', async () => {
|
|
477
550
|
const job = await createListeningJob({status: 'running', listenerStatus: 'listening'});
|
|
478
551
|
await bufferEvent(job.id);
|
|
@@ -493,6 +566,97 @@ describe('drainListenerEventsIntoExecution', () => {
|
|
|
493
566
|
expect(events.every((event) => event.consumedByExecutionId === executions[0]?.id)).toBe(true);
|
|
494
567
|
});
|
|
495
568
|
|
|
569
|
+
it('persists a failed execution when listener variables are missing', async () => {
|
|
570
|
+
const job = await createListeningJobFromModel({
|
|
571
|
+
jobs: {
|
|
572
|
+
review: {
|
|
573
|
+
executionName: `Review ${template('vars.REGION')}`,
|
|
574
|
+
steps: [{run: `echo ${template('vars.MISSING')}`}],
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
await bufferEvent(job.id);
|
|
579
|
+
|
|
580
|
+
const result = await drainListenerEventsIntoExecution({
|
|
581
|
+
jobId: job.id,
|
|
582
|
+
expectedSequence: 1,
|
|
583
|
+
secrets: {
|
|
584
|
+
getVariablesByNamespace: async () => ({values: {}}),
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const [execution] = await db()
|
|
589
|
+
.select()
|
|
590
|
+
.from(jobExecutions)
|
|
591
|
+
.where(eq(jobExecutions.jobId, job.id));
|
|
592
|
+
expect(result).toMatchObject({kind: 'execution', sequence: 1, status: 'failed'});
|
|
593
|
+
expect(execution).toMatchObject({
|
|
594
|
+
status: 'failed',
|
|
595
|
+
name: null,
|
|
596
|
+
evaluationTrace: [
|
|
597
|
+
expect.objectContaining({
|
|
598
|
+
expression: 'vars.MISSING',
|
|
599
|
+
roots: ['vars'],
|
|
600
|
+
fillTarget: 'execution-creation',
|
|
601
|
+
evaluatedAt: 'execution-creation',
|
|
602
|
+
field: 'run',
|
|
603
|
+
degraded: true,
|
|
604
|
+
}),
|
|
605
|
+
],
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('resolves a distinct name for each listener firing with boundary variables', async () => {
|
|
610
|
+
const job = await createListeningJobFromModel({
|
|
611
|
+
jobs: {
|
|
612
|
+
review: {
|
|
613
|
+
name: 'Process review',
|
|
614
|
+
executionName: `Review ${template('vars.REGION')} ${template('execution.index')}`,
|
|
615
|
+
steps: [{run: 'echo review'}],
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
await db().update(jobs).set({name: 'Process review'}).where(eq(jobs.id, job.id));
|
|
620
|
+
const secrets = {
|
|
621
|
+
getVariablesByNamespace: async () => ({values: {REGION: 'eu-west'}}),
|
|
622
|
+
};
|
|
623
|
+
await bufferEvent(job.id);
|
|
624
|
+
await drainListenerEventsIntoExecution({
|
|
625
|
+
jobId: job.id,
|
|
626
|
+
expectedSequence: 1,
|
|
627
|
+
secrets,
|
|
628
|
+
});
|
|
629
|
+
await bufferEvent(job.id);
|
|
630
|
+
await drainListenerEventsIntoExecution({
|
|
631
|
+
jobId: job.id,
|
|
632
|
+
expectedSequence: 2,
|
|
633
|
+
secrets,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const executions = await db()
|
|
637
|
+
.select()
|
|
638
|
+
.from(jobExecutions)
|
|
639
|
+
.where(eq(jobExecutions.jobId, job.id))
|
|
640
|
+
.orderBy(asc(jobExecutions.sequence));
|
|
641
|
+
const stored = await readJob(job.id);
|
|
642
|
+
|
|
643
|
+
expect(stored?.name).toBe('Process review');
|
|
644
|
+
expect(executions.map((execution) => execution.name)).toEqual([
|
|
645
|
+
'Review eu-west 0',
|
|
646
|
+
'Review eu-west 1',
|
|
647
|
+
]);
|
|
648
|
+
expect(executions.map((execution) => execution.evaluationTrace)).toEqual([
|
|
649
|
+
expect.arrayContaining([
|
|
650
|
+
expect.objectContaining({field: 'job.execution_name', expression: 'vars.REGION'}),
|
|
651
|
+
expect.objectContaining({field: 'job.execution_name', expression: 'execution.index'}),
|
|
652
|
+
]),
|
|
653
|
+
expect.arrayContaining([
|
|
654
|
+
expect.objectContaining({field: 'job.execution_name', expression: 'vars.REGION'}),
|
|
655
|
+
expect.objectContaining({field: 'job.execution_name', expression: 'execution.index'}),
|
|
656
|
+
]),
|
|
657
|
+
]);
|
|
658
|
+
});
|
|
659
|
+
|
|
496
660
|
it('uses the frozen agent tool materialization snapshot for listener executions', async () => {
|
|
497
661
|
const model = workflowModel({
|
|
498
662
|
jobs: {
|
|
@@ -823,12 +987,30 @@ describe('drainListenerEventsIntoExecution', () => {
|
|
|
823
987
|
expect(stored?.outputs).toEqual({message: 'hello'});
|
|
824
988
|
});
|
|
825
989
|
|
|
826
|
-
it('reports empty when nothing is buffered', async () => {
|
|
827
|
-
const job = await
|
|
990
|
+
it('reports empty without loading external state when nothing is buffered', async () => {
|
|
991
|
+
const job = await createListeningJobFromModel({
|
|
992
|
+
jobs: {
|
|
993
|
+
review: {
|
|
994
|
+
executionName: template('vars.REGION'),
|
|
995
|
+
steps: [{run: 'echo review'}],
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
let variableLoads = 0;
|
|
828
1000
|
|
|
829
|
-
const result = await drainListenerEventsIntoExecution({
|
|
1001
|
+
const result = await drainListenerEventsIntoExecution({
|
|
1002
|
+
jobId: job.id,
|
|
1003
|
+
expectedSequence: 1,
|
|
1004
|
+
secrets: {
|
|
1005
|
+
getVariablesByNamespace: () => {
|
|
1006
|
+
variableLoads += 1;
|
|
1007
|
+
return Promise.resolve({values: {REGION: 'eu-west'}});
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
830
1011
|
|
|
831
1012
|
expect(result).toEqual({kind: 'empty'});
|
|
1013
|
+
expect(variableLoads).toBe(0);
|
|
832
1014
|
});
|
|
833
1015
|
|
|
834
1016
|
it('returns the existing execution when the sequence was already materialized', async () => {
|
package/src/db/job-listeners.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
|
|
|
2
2
|
import {readPersistedWorkflowModel} from '@shipfox/api-definitions-dto';
|
|
3
3
|
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
4
4
|
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
5
|
+
import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
|
|
5
6
|
import {
|
|
6
7
|
WORKFLOWS_JOB_ACTIVATED,
|
|
7
8
|
type WorkflowsJobActivatedEventDto,
|
|
@@ -10,7 +11,13 @@ import {and, asc, count, eq, inArray, isNull, notInArray, sql} from 'drizzle-orm
|
|
|
10
11
|
import {type AgentDefaultsResolver, createAgentDefaultsResolver} from '#core/agent-defaults.js';
|
|
11
12
|
import {loadAgentToolMaterializationContext} from '#core/agent-tools.js';
|
|
12
13
|
import {isJobTerminal, type JobStatus, type ResolutionReason} from '#core/entities/job.js';
|
|
13
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
JobExecution,
|
|
16
|
+
JobExecutionStatus,
|
|
17
|
+
WorkflowExecutionEvent,
|
|
18
|
+
} from '#core/entities/job-execution.js';
|
|
19
|
+
import {normalizeWorkflowExecutionEvent} from '#core/entities/job-execution.js';
|
|
20
|
+
import {InterpolationUnresolvableError} from '#core/errors.js';
|
|
14
21
|
import {type DeriveJobSuccessResult, deriveJobSuccess} from '#core/job-transition/index.js';
|
|
15
22
|
import {
|
|
16
23
|
type MaterializedListenerExecution,
|
|
@@ -38,6 +45,7 @@ import {toWorkflowRun, workflowRuns} from './schema/workflow-runs.js';
|
|
|
38
45
|
import {
|
|
39
46
|
bulkUpdateStepStatuses,
|
|
40
47
|
getDirectDependencyJobContexts,
|
|
48
|
+
loadReferencedVariables,
|
|
41
49
|
updateJobStatusAtVersion,
|
|
42
50
|
} from './workflow-runs.js';
|
|
43
51
|
|
|
@@ -74,7 +82,7 @@ export async function activateJobListener(
|
|
|
74
82
|
): Promise<ActivateJobListenerResult> {
|
|
75
83
|
return await db().transaction(async (tx) => {
|
|
76
84
|
const [target] = await tx
|
|
77
|
-
.select({job: jobs, run: workflowRuns})
|
|
85
|
+
.select({job: jobs, attempt: workflowRunAttempts, run: workflowRuns})
|
|
78
86
|
.from(jobs)
|
|
79
87
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
80
88
|
.innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
|
|
@@ -138,6 +146,7 @@ export async function activateJobListener(
|
|
|
138
146
|
run: toWorkflowRun(target.run),
|
|
139
147
|
triggerPayload: target.run.triggerPayload,
|
|
140
148
|
inputs: target.run.inputs,
|
|
149
|
+
vars: target.attempt.vars ?? undefined,
|
|
141
150
|
plan: snapshotPlan,
|
|
142
151
|
dependencyJobs,
|
|
143
152
|
});
|
|
@@ -182,11 +191,61 @@ export interface DrainListenerEventsParams {
|
|
|
182
191
|
projects?: ProjectsModuleClient | undefined;
|
|
183
192
|
resolveAgentDefaults?: AgentDefaultsResolver | undefined;
|
|
184
193
|
agent?: AgentInterModuleClient | undefined;
|
|
194
|
+
secrets?: Pick<SecretsInterModuleClient, 'getVariablesByNamespace'> | undefined;
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
export async function drainListenerEventsIntoExecution(
|
|
188
198
|
params: DrainListenerEventsParams,
|
|
189
199
|
): Promise<DrainListenerEventsResult> {
|
|
200
|
+
const probeResult = await db().transaction(async (tx) => {
|
|
201
|
+
const existing = await findExistingExecution(params, tx);
|
|
202
|
+
if (existing) return existing;
|
|
203
|
+
|
|
204
|
+
const resolveRequested = await hasPendingResolveEvent(params.jobId, tx);
|
|
205
|
+
if (resolveRequested) return {kind: 'resolve-requested' as const};
|
|
206
|
+
|
|
207
|
+
const hasBufferedFire = await hasBufferedFireEvent(params.jobId, tx);
|
|
208
|
+
return hasBufferedFire ? null : {kind: 'empty' as const};
|
|
209
|
+
});
|
|
210
|
+
if (probeResult !== null) return probeResult;
|
|
211
|
+
|
|
212
|
+
// Resolve external state before opening the persistence transaction. The
|
|
213
|
+
// secrets module uses the shared pool and cannot reuse this transaction's
|
|
214
|
+
// connection across the inter-module boundary.
|
|
215
|
+
const materializationTarget = await loadListenerMaterializationTarget(params.jobId);
|
|
216
|
+
const model =
|
|
217
|
+
materializationTarget.attempt.model === null
|
|
218
|
+
? null
|
|
219
|
+
: readPersistedWorkflowModel(materializationTarget.attempt.model);
|
|
220
|
+
const modelJob = model?.jobs.find((job) => job.key === materializationTarget.job.key);
|
|
221
|
+
let vars: Record<string, string> | undefined;
|
|
222
|
+
let variableResolutionError: InterpolationUnresolvableError | undefined;
|
|
223
|
+
if (model !== null && modelJob !== undefined) {
|
|
224
|
+
try {
|
|
225
|
+
vars = await loadReferencedVariables({
|
|
226
|
+
model,
|
|
227
|
+
jobs: [modelJob],
|
|
228
|
+
workspaceId: materializationTarget.run.workspaceId,
|
|
229
|
+
projectId: materializationTarget.run.projectId,
|
|
230
|
+
definitionId: materializationTarget.run.definitionId,
|
|
231
|
+
secrets: params.secrets,
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!(error instanceof InterpolationUnresolvableError)) throw error;
|
|
235
|
+
variableResolutionError = error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const agentToolContext =
|
|
239
|
+
materializationTarget.attempt.agentToolMaterialization === null
|
|
240
|
+
? await loadAgentToolMaterializationContext({
|
|
241
|
+
model,
|
|
242
|
+
workspaceId: materializationTarget.run.workspaceId,
|
|
243
|
+
projectId: materializationTarget.run.projectId,
|
|
244
|
+
integrations: params.integrations,
|
|
245
|
+
projects: params.projects,
|
|
246
|
+
})
|
|
247
|
+
: undefined;
|
|
248
|
+
|
|
190
249
|
const drained = await db().transaction(async (tx) => {
|
|
191
250
|
const existing = await findExistingExecution(params, tx);
|
|
192
251
|
if (existing) return {result: existing};
|
|
@@ -198,25 +257,20 @@ export async function drainListenerEventsIntoExecution(
|
|
|
198
257
|
if (bufferedEvents.length === 0) return {result: {kind: 'empty' as const}};
|
|
199
258
|
|
|
200
259
|
const target = await loadListenerMaterializationTarget(params.jobId, tx);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
model,
|
|
207
|
-
workspaceId: target.run.workspaceId,
|
|
208
|
-
projectId: target.run.projectId,
|
|
209
|
-
integrations: params.integrations,
|
|
210
|
-
projects: params.projects,
|
|
211
|
-
})
|
|
212
|
-
: undefined;
|
|
260
|
+
const priorExecutions = await loadListenerPriorExecutions(
|
|
261
|
+
params.jobId,
|
|
262
|
+
target.job.name ?? target.job.key,
|
|
263
|
+
tx,
|
|
264
|
+
);
|
|
213
265
|
const materialized = await materializeListenerExecution({
|
|
214
266
|
model,
|
|
215
267
|
run: toWorkflowRun(target.run),
|
|
216
268
|
job: toJob(target.job),
|
|
269
|
+
vars,
|
|
270
|
+
variableResolutionError,
|
|
217
271
|
sequence: params.expectedSequence,
|
|
218
272
|
triggerEvents: listenerTriggerEvents(bufferedEvents),
|
|
219
|
-
priorExecutions
|
|
273
|
+
priorExecutions,
|
|
220
274
|
resolveAgentDefaults:
|
|
221
275
|
params.resolveAgentDefaults ??
|
|
222
276
|
(params.agent
|
|
@@ -315,7 +369,13 @@ type ApplyJobListenerResolutionResult =
|
|
|
315
369
|
async function deriveJobListenerResolutionDecision(
|
|
316
370
|
jobId: string,
|
|
317
371
|
): Promise<JobListenerResolutionDecision> {
|
|
318
|
-
const [
|
|
372
|
+
const [target] = await db()
|
|
373
|
+
.select({job: jobs, attempt: workflowRunAttempts})
|
|
374
|
+
.from(jobs)
|
|
375
|
+
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
376
|
+
.where(eq(jobs.id, jobId))
|
|
377
|
+
.limit(1);
|
|
378
|
+
const jobRow = target?.job;
|
|
319
379
|
if (!jobRow) throw new Error(`Job not found: ${jobId}`);
|
|
320
380
|
|
|
321
381
|
const [executionRows, dependencyJobs] = await Promise.all([
|
|
@@ -330,8 +390,11 @@ async function deriveJobListenerResolutionDecision(
|
|
|
330
390
|
expectedVersion: jobRow.version,
|
|
331
391
|
...deriveJobSuccess({
|
|
332
392
|
success: jobRow.success,
|
|
333
|
-
executions: executionRows.map(
|
|
393
|
+
executions: executionRows.map((execution) =>
|
|
394
|
+
toJobExecution(execution, jobRow.name ?? jobRow.key),
|
|
395
|
+
),
|
|
334
396
|
jobs: dependencyJobs,
|
|
397
|
+
vars: target.attempt.vars ?? undefined,
|
|
335
398
|
}),
|
|
336
399
|
};
|
|
337
400
|
}
|
|
@@ -462,6 +525,21 @@ async function hasPendingResolveEvent(jobId: string, tx: Tx): Promise<boolean> {
|
|
|
462
525
|
return resolveEvent !== undefined;
|
|
463
526
|
}
|
|
464
527
|
|
|
528
|
+
async function hasBufferedFireEvent(jobId: string, tx: Tx): Promise<boolean> {
|
|
529
|
+
const [fireEvent] = await tx
|
|
530
|
+
.select({id: jobListenerEvents.id})
|
|
531
|
+
.from(jobListenerEvents)
|
|
532
|
+
.where(
|
|
533
|
+
and(
|
|
534
|
+
eq(jobListenerEvents.jobId, jobId),
|
|
535
|
+
eq(jobListenerEvents.disposition, 'fire'),
|
|
536
|
+
isNull(jobListenerEvents.consumedByExecutionId),
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
.limit(1);
|
|
540
|
+
return fireEvent !== undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
465
543
|
async function lockBufferedFireEvents(
|
|
466
544
|
params: DrainListenerEventsParams,
|
|
467
545
|
tx: Tx,
|
|
@@ -486,13 +564,19 @@ async function lockBufferedFireEvents(
|
|
|
486
564
|
function listenerTriggerEvents(
|
|
487
565
|
bufferedEvents: readonly JobListenerEventDb[],
|
|
488
566
|
): WorkflowExecutionEvent[] {
|
|
489
|
-
return bufferedEvents.map((event) =>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
567
|
+
return bufferedEvents.map((event) =>
|
|
568
|
+
normalizeWorkflowExecutionEvent({
|
|
569
|
+
source: event.source,
|
|
570
|
+
event: event.event,
|
|
571
|
+
delivery_id: event.deliveryId,
|
|
572
|
+
received_at: event.receivedAt.toISOString(),
|
|
573
|
+
project: event.triggerReference?.project ?? null,
|
|
574
|
+
repository: event.triggerReference?.repository ?? null,
|
|
575
|
+
ref: event.triggerReference?.ref ?? null,
|
|
576
|
+
commit: event.triggerReference?.commit ?? null,
|
|
577
|
+
data: event.payload,
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
496
580
|
}
|
|
497
581
|
|
|
498
582
|
async function persistMaterializedListenerExecution(
|
|
@@ -509,7 +593,7 @@ async function persistMaterializedListenerExecution(
|
|
|
509
593
|
.values({
|
|
510
594
|
jobId: params.jobId,
|
|
511
595
|
sequence: params.sequence,
|
|
512
|
-
name: params.materialized.
|
|
596
|
+
name: params.materialized.nameOverride,
|
|
513
597
|
runner: params.materialized.runner.length === 0 ? null : [...params.materialized.runner],
|
|
514
598
|
status: params.materialized.status,
|
|
515
599
|
statusReason: params.materialized.statusReason,
|
|
@@ -559,21 +643,29 @@ function drainExecutionResult(
|
|
|
559
643
|
};
|
|
560
644
|
}
|
|
561
645
|
|
|
562
|
-
async function loadListenerMaterializationTarget(jobId: string, tx
|
|
563
|
-
const
|
|
646
|
+
async function loadListenerMaterializationTarget(jobId: string, tx?: Tx) {
|
|
647
|
+
const query = (tx ?? db())
|
|
564
648
|
.select({job: jobs, attempt: workflowRunAttempts, run: workflowRuns})
|
|
565
649
|
.from(jobs)
|
|
566
650
|
.innerJoin(workflowRunAttempts, eq(jobs.workflowRunAttemptId, workflowRunAttempts.id))
|
|
567
651
|
.innerJoin(workflowRuns, eq(workflowRunAttempts.workflowRunId, workflowRuns.id))
|
|
568
652
|
.where(eq(jobs.id, jobId))
|
|
569
|
-
.limit(1)
|
|
570
|
-
|
|
653
|
+
.limit(1);
|
|
654
|
+
const [target] = tx === undefined ? await query : await query.for('update');
|
|
571
655
|
if (!target) throw new Error(`Job not found: ${jobId}`);
|
|
572
656
|
|
|
657
|
+
return target;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function loadListenerPriorExecutions(
|
|
661
|
+
jobId: string,
|
|
662
|
+
fallbackName: string,
|
|
663
|
+
tx: Tx,
|
|
664
|
+
): Promise<JobExecution[]> {
|
|
573
665
|
const priorExecutions = await tx
|
|
574
666
|
.select()
|
|
575
667
|
.from(jobExecutions)
|
|
576
668
|
.where(eq(jobExecutions.jobId, jobId))
|
|
577
669
|
.orderBy(asc(jobExecutions.sequence), asc(jobExecutions.id));
|
|
578
|
-
return
|
|
670
|
+
return priorExecutions.map((execution) => toJobExecution(execution, fallbackName));
|
|
579
671
|
}
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
uuid,
|
|
12
12
|
} from 'drizzle-orm/pg-core';
|
|
13
13
|
import {toJobStatusReason} from '#core/entities/job.js';
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
type JobExecution,
|
|
16
|
+
normalizeWorkflowExecutionEvent,
|
|
17
|
+
type WorkflowExecutionEvent,
|
|
18
|
+
} from '#core/entities/job-execution.js';
|
|
15
19
|
import type {PersistedEvaluationTraceEntry} from '#core/entities/step.js';
|
|
16
20
|
import {pgTable} from './common.js';
|
|
17
21
|
import {jobStatusReasonEnum, jobs} from './jobs.js';
|
|
@@ -32,7 +36,9 @@ export const jobExecutions = pgTable(
|
|
|
32
36
|
.notNull()
|
|
33
37
|
.references(() => jobs.id, {onDelete: 'cascade'}),
|
|
34
38
|
sequence: integer('sequence').notNull(),
|
|
35
|
-
|
|
39
|
+
// A null value means no dynamic override was resolved. The core entity
|
|
40
|
+
// exposes the parent job's static name/key as the effective name.
|
|
41
|
+
name: text('name'),
|
|
36
42
|
runner: jsonb('runner').$type<string[]>(),
|
|
37
43
|
status: jobExecutionStatusEnum('status').notNull().default('pending'),
|
|
38
44
|
statusReason: jobStatusReasonEnum('status_reason'),
|
|
@@ -62,16 +68,17 @@ export const jobExecutions = pgTable(
|
|
|
62
68
|
export type JobExecutionDb = typeof jobExecutions.$inferSelect;
|
|
63
69
|
export type JobExecutionCreateDb = typeof jobExecutions.$inferInsert;
|
|
64
70
|
|
|
65
|
-
export function toJobExecution(row: JobExecutionDb): JobExecution {
|
|
71
|
+
export function toJobExecution(row: JobExecutionDb, fallbackName: string): JobExecution {
|
|
66
72
|
return {
|
|
67
73
|
id: row.id,
|
|
68
74
|
jobId: row.jobId,
|
|
69
75
|
sequence: row.sequence,
|
|
70
|
-
|
|
76
|
+
nameOverride: row.name,
|
|
77
|
+
name: row.name ?? fallbackName,
|
|
71
78
|
runner: row.runner as string[] | null,
|
|
72
79
|
status: row.status,
|
|
73
80
|
statusReason: toJobStatusReason(row.statusReason),
|
|
74
|
-
triggerEvents: row.triggerEvents,
|
|
81
|
+
triggerEvents: row.triggerEvents.map(normalizeWorkflowExecutionEvent),
|
|
75
82
|
outputs: row.outputs,
|
|
76
83
|
evaluationTrace: row.evaluationTrace ?? null,
|
|
77
84
|
version: row.version,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
2
|
import {index, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
3
3
|
import type {JobListenerEvent} from '#core/entities/job-listener-event.js';
|
|
4
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
4
5
|
import {pgTable} from './common.js';
|
|
5
6
|
import {jobExecutions} from './job-executions.js';
|
|
6
7
|
import {jobs} from './jobs.js';
|
|
@@ -22,6 +23,7 @@ export const jobListenerEvents = pgTable(
|
|
|
22
23
|
deliveryId: text('delivery_id').notNull(),
|
|
23
24
|
source: text('source').notNull(),
|
|
24
25
|
event: text('event').notNull(),
|
|
26
|
+
triggerReference: jsonb('trigger_reference').$type<WorkflowRunTriggerReference>(),
|
|
25
27
|
payload: jsonb('payload').notNull(),
|
|
26
28
|
receivedAt: timestamp('received_at', {withTimezone: true}).notNull(),
|
|
27
29
|
consumedByExecutionId: uuid('consumed_by_execution_id').references(() => jobExecutions.id, {
|
|
@@ -50,6 +52,7 @@ export function toJobListenerEvent(row: JobListenerEventDb): JobListenerEvent {
|
|
|
50
52
|
deliveryId: row.deliveryId,
|
|
51
53
|
source: row.source,
|
|
52
54
|
event: row.event,
|
|
55
|
+
triggerReference: row.triggerReference ?? null,
|
|
53
56
|
payload: row.payload,
|
|
54
57
|
receivedAt: row.receivedAt,
|
|
55
58
|
consumedByExecutionId: row.consumedByExecutionId,
|
|
@@ -21,8 +21,8 @@ import {stepStatusEnum, steps} from './steps.js';
|
|
|
21
21
|
|
|
22
22
|
// Append-only execution history. One row per dispatched attempt of a step,
|
|
23
23
|
// inserted `running` at dispatch and finalized terminal at report. `steps` holds
|
|
24
|
-
// the fast current projection; this table is the audit trail
|
|
25
|
-
//
|
|
24
|
+
// the fast current projection; this table is the audit trail. The unique
|
|
25
|
+
// (step_id, attempt) constraint is the idempotency anchor.
|
|
26
26
|
export const stepAttempts = pgTable(
|
|
27
27
|
'step_attempts',
|
|
28
28
|
{
|
package/src/db/schema/steps.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
STEP_STATUS_REASONS,
|
|
19
19
|
type Step,
|
|
20
20
|
type StepConfigDispatchPlan,
|
|
21
|
+
type StepType,
|
|
21
22
|
toStepStatusReason,
|
|
22
23
|
} from '#core/entities/step.js';
|
|
23
24
|
import {pgTable} from './common.js';
|
|
@@ -45,7 +46,7 @@ export const steps = pgTable(
|
|
|
45
46
|
status: stepStatusEnum('status').notNull().default('pending'),
|
|
46
47
|
statusReason: stepStatusReasonEnum('status_reason'),
|
|
47
48
|
evaluationTrace: jsonb('evaluation_trace').$type<readonly PersistedEvaluationTraceEntry[]>(),
|
|
48
|
-
type: text('type').notNull(),
|
|
49
|
+
type: text('type').notNull().$type<StepType>(),
|
|
49
50
|
config: jsonb('config').notNull().$type<Record<string, unknown>>(),
|
|
50
51
|
condition: jsonb('condition').$type<WorkflowExpression>(),
|
|
51
52
|
configPlan: jsonb('config_plan').$type<StepConfigDispatchPlan>(),
|