@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,19 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type {WorkflowModel} from '@shipfox/api-definitions-dto';
|
|
2
|
+
import {
|
|
3
|
+
type IntegrationsModuleClient,
|
|
4
|
+
integrationsInterModuleContract,
|
|
5
|
+
} from '@shipfox/api-integration-core-dto/inter-module';
|
|
6
|
+
import {
|
|
7
|
+
type ProjectsModuleClient,
|
|
8
|
+
projectsInterModuleContract,
|
|
9
|
+
} from '@shipfox/api-projects-dto/inter-module';
|
|
3
10
|
import {createInterModuleKnownError} from '@shipfox/inter-module';
|
|
4
11
|
import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
|
|
5
12
|
import {createCapturingLogger} from '@shipfox/node-log/test';
|
|
6
|
-
import {
|
|
13
|
+
import {eq} from 'drizzle-orm';
|
|
14
|
+
import type {StepStatus} from '#core/entities/step.js';
|
|
15
|
+
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
16
|
+
import {db} from '#db/db.js';
|
|
17
|
+
import {jobs as jobsTable} from '#db/schema/jobs.js';
|
|
18
|
+
import {steps as stepsTable} from '#db/schema/steps.js';
|
|
19
|
+
import {workflowRuns} from '#db/schema/workflow-runs.js';
|
|
20
|
+
import {createWorkflowRun, getJobsByWorkflowRunId, getStepsByJobId} from '#db/workflow-runs.js';
|
|
7
21
|
import {projectFactory} from '#test/factories/project.js';
|
|
8
|
-
import {
|
|
22
|
+
import {workflowModel} from '#test/factories/workflow-model.js';
|
|
23
|
+
import {insertRunningJobLease, mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
|
|
9
24
|
import {fakeLeaseTokenAuthMethod, mintLeaseToken} from '#test/fixtures/lease-token.js';
|
|
10
25
|
import {runnersTestClient} from '#test/fixtures/runners-inter-module.js';
|
|
11
26
|
import {createLeaseTokenRouteGroup} from './index.js';
|
|
12
27
|
|
|
13
28
|
const getProjectById = vi.fn();
|
|
14
|
-
const
|
|
29
|
+
const resolveCheckoutTarget = vi.fn();
|
|
30
|
+
const projects = {
|
|
31
|
+
getProjectById,
|
|
32
|
+
resolveCheckoutTarget,
|
|
33
|
+
} as Pick<ProjectsModuleClient, 'getProjectById' | 'resolveCheckoutTarget'>;
|
|
15
34
|
|
|
16
|
-
const
|
|
35
|
+
const createCheckoutSpec = vi.fn();
|
|
36
|
+
const integrations = {
|
|
37
|
+
createCheckoutSpec,
|
|
38
|
+
} as Pick<IntegrationsModuleClient, 'createCheckoutSpec'>;
|
|
39
|
+
|
|
40
|
+
const {logger, lines: logLines, clear: clearLogLines} = createCapturingLogger();
|
|
17
41
|
|
|
18
42
|
const githubSpec = (token: string) => ({
|
|
19
43
|
repositoryUrl: 'https://github.com/acme/repo.git',
|
|
@@ -21,10 +45,8 @@ const githubSpec = (token: string) => ({
|
|
|
21
45
|
credentials: {username: 'x-access-token', token, expiresAt: new Date('2026-06-10T12:00:00.000Z')},
|
|
22
46
|
});
|
|
23
47
|
|
|
24
|
-
describe('POST /runs/jobs/current/checkout-token', () => {
|
|
48
|
+
describe('POST /runs/jobs/current/steps/:stepId/checkout-token', () => {
|
|
25
49
|
let app: FastifyInstance;
|
|
26
|
-
const createCheckoutSpec = vi.fn();
|
|
27
|
-
const {logger, lines: logLines, clear: clearLogLines} = createCapturingLogger();
|
|
28
50
|
|
|
29
51
|
beforeAll(async () => {
|
|
30
52
|
app = await createApp({
|
|
@@ -34,7 +56,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
34
56
|
agent: {} as never,
|
|
35
57
|
annotations: {} as never,
|
|
36
58
|
auth: {} as never,
|
|
37
|
-
integrations:
|
|
59
|
+
integrations: integrations as never,
|
|
38
60
|
projects: projects as never,
|
|
39
61
|
runners: runnersTestClient,
|
|
40
62
|
secrets: {} as never,
|
|
@@ -49,6 +71,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
49
71
|
beforeEach(() => {
|
|
50
72
|
createCheckoutSpec.mockReset();
|
|
51
73
|
getProjectById.mockReset();
|
|
74
|
+
resolveCheckoutTarget.mockReset();
|
|
52
75
|
clearLogLines();
|
|
53
76
|
});
|
|
54
77
|
|
|
@@ -56,25 +79,39 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
56
79
|
await closeApp();
|
|
57
80
|
});
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
test('rejects a request without an Authorization header', async () => {
|
|
83
|
+
const res = await app.inject({
|
|
84
|
+
method: 'POST',
|
|
85
|
+
url: checkoutUrl(crypto.randomUUID(), 1),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(res.statusCode).toBe(401);
|
|
89
|
+
expect(res.json().code).toBe('unauthorized');
|
|
90
|
+
});
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
test('does not keep the old job-scoped route as a compatibility alias', async () => {
|
|
93
|
+
const res = await app.inject({
|
|
94
|
+
method: 'POST',
|
|
95
|
+
url: '/runs/jobs/current/checkout-token',
|
|
65
96
|
});
|
|
97
|
+
|
|
98
|
+
expect(res.statusCode).toBe(404);
|
|
66
99
|
});
|
|
67
100
|
|
|
68
|
-
test('
|
|
69
|
-
const project =
|
|
101
|
+
test('mints against the frozen setup-step config and returns its default fetch depth', async () => {
|
|
102
|
+
const {project, job, step} = await createRunningCheckoutStep();
|
|
70
103
|
getProjectById.mockResolvedValue({project});
|
|
71
|
-
|
|
104
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
105
|
+
projectId: project.id,
|
|
106
|
+
connectionId: project.sourceConnectionId,
|
|
107
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
108
|
+
});
|
|
72
109
|
createCheckoutSpec.mockResolvedValue(githubSpec('ghs-secret-token'));
|
|
73
110
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
74
111
|
|
|
75
112
|
const res = await app.inject({
|
|
76
113
|
method: 'POST',
|
|
77
|
-
url:
|
|
114
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
78
115
|
headers: {authorization: `Bearer ${token}`},
|
|
79
116
|
});
|
|
80
117
|
|
|
@@ -83,6 +120,7 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
83
120
|
expect(res.json()).toEqual({
|
|
84
121
|
repository_url: 'https://github.com/acme/repo.git',
|
|
85
122
|
ref: 'main',
|
|
123
|
+
fetch_depth: 1,
|
|
86
124
|
auth: {
|
|
87
125
|
kind: 'basic',
|
|
88
126
|
username: 'x-access-token',
|
|
@@ -93,255 +131,342 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
93
131
|
persist: true,
|
|
94
132
|
},
|
|
95
133
|
});
|
|
134
|
+
expect(resolveCheckoutTarget).toHaveBeenCalledWith({
|
|
135
|
+
workspaceId: project.workspaceId,
|
|
136
|
+
defaults: {connectionId: project.sourceConnectionId, owner: 'acme'},
|
|
137
|
+
target: {project: project.id},
|
|
138
|
+
});
|
|
139
|
+
expect(createCheckoutSpec).toHaveBeenCalledWith({
|
|
140
|
+
workspaceId: project.workspaceId,
|
|
141
|
+
connectionId: project.sourceConnectionId,
|
|
142
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
143
|
+
permissions: {contents: 'read'},
|
|
144
|
+
});
|
|
96
145
|
});
|
|
97
146
|
|
|
98
|
-
test('
|
|
99
|
-
const project =
|
|
147
|
+
test('defaults the checkout ref to the run trigger commit for the same project', async () => {
|
|
148
|
+
const {run, project, job, step} = await createRunningCheckoutStep();
|
|
149
|
+
const triggerReference = {
|
|
150
|
+
project: {id: project.id},
|
|
151
|
+
repository: 'acme/repo',
|
|
152
|
+
ref: 'refs/heads/feature/checkout',
|
|
153
|
+
commit: 'a'.repeat(40),
|
|
154
|
+
actor: null,
|
|
155
|
+
} satisfies WorkflowRunTriggerReference;
|
|
156
|
+
await db().update(workflowRuns).set({triggerReference}).where(eq(workflowRuns.id, run.id));
|
|
100
157
|
getProjectById.mockResolvedValue({project});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
158
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
159
|
+
projectId: project.id,
|
|
160
|
+
connectionId: project.sourceConnectionId,
|
|
161
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
105
162
|
});
|
|
163
|
+
createCheckoutSpec.mockResolvedValue(githubSpec('ghs-trigger-token'));
|
|
106
164
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
107
165
|
|
|
108
166
|
const res = await app.inject({
|
|
109
167
|
method: 'POST',
|
|
110
|
-
url:
|
|
168
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
111
169
|
headers: {authorization: `Bearer ${token}`},
|
|
112
170
|
});
|
|
113
171
|
|
|
114
172
|
expect(res.statusCode).toBe(200);
|
|
115
|
-
expect(
|
|
116
|
-
|
|
117
|
-
|
|
173
|
+
expect(createCheckoutSpec).toHaveBeenCalledWith({
|
|
174
|
+
workspaceId: project.workspaceId,
|
|
175
|
+
connectionId: project.sourceConnectionId,
|
|
176
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
177
|
+
ref: triggerReference.commit,
|
|
178
|
+
permissions: {contents: 'read'},
|
|
118
179
|
});
|
|
119
180
|
});
|
|
120
181
|
|
|
121
|
-
test('returns
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
182
|
+
test('returns checkout-unavailable when the run project no longer resolves', async () => {
|
|
183
|
+
const {job, step} = await createRunningCheckoutStep({kind: 'checkout'});
|
|
184
|
+
getProjectById.mockResolvedValue({project: null});
|
|
185
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
126
186
|
|
|
127
187
|
const res = await app.inject({
|
|
128
188
|
method: 'POST',
|
|
129
|
-
url:
|
|
189
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
130
190
|
headers: {authorization: `Bearer ${token}`},
|
|
131
191
|
});
|
|
132
192
|
|
|
133
193
|
expect(res.statusCode).toBe(404);
|
|
134
|
-
expect(res.json().code).toBe('
|
|
194
|
+
expect(res.json().code).toBe('checkout-unavailable');
|
|
195
|
+
expect(resolveCheckoutTarget).not.toHaveBeenCalled();
|
|
135
196
|
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
136
197
|
});
|
|
137
198
|
|
|
138
|
-
test
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const project = projectFactory.build();
|
|
145
|
-
getProjectById.mockResolvedValue({project});
|
|
146
|
-
const job = await jobFactory.create({}, {transient: {projectId: project.id, status}});
|
|
147
|
-
const token = await mintLeaseToken({jobId: job.id});
|
|
199
|
+
test('returns a client error for a malformed project target', async () => {
|
|
200
|
+
const {job, step} = await createRunningCheckoutStep({
|
|
201
|
+
kind: 'checkout',
|
|
202
|
+
checkout: {project: 'not-a-uuid'},
|
|
203
|
+
});
|
|
204
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
148
205
|
|
|
149
206
|
const res = await app.inject({
|
|
150
207
|
method: 'POST',
|
|
151
|
-
url:
|
|
208
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
152
209
|
headers: {authorization: `Bearer ${token}`},
|
|
153
210
|
});
|
|
154
211
|
|
|
155
|
-
expect(res.statusCode).toBe(
|
|
156
|
-
expect(res.json().code).toBe('
|
|
212
|
+
expect(res.statusCode).toBe(409);
|
|
213
|
+
expect(res.json().code).toBe('checkout-config-invalid');
|
|
214
|
+
expect(getProjectById).not.toHaveBeenCalled();
|
|
215
|
+
expect(resolveCheckoutTarget).not.toHaveBeenCalled();
|
|
157
216
|
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
158
217
|
});
|
|
159
218
|
|
|
160
|
-
test('
|
|
161
|
-
const project =
|
|
219
|
+
test('omits auth for a credential-free checkout spec', async () => {
|
|
220
|
+
const {project, job, step} = await createRunningCheckoutStep();
|
|
162
221
|
getProjectById.mockResolvedValue({project});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
222
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
223
|
+
projectId: project.id,
|
|
224
|
+
connectionId: project.sourceConnectionId,
|
|
225
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
226
|
+
});
|
|
227
|
+
createCheckoutSpec.mockResolvedValue({
|
|
228
|
+
repositoryUrl: 'https://example.com/acme/repo.git',
|
|
229
|
+
ref: 'trunk',
|
|
230
|
+
});
|
|
168
231
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
169
232
|
|
|
170
233
|
const res = await app.inject({
|
|
171
234
|
method: 'POST',
|
|
172
|
-
url:
|
|
235
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
173
236
|
headers: {authorization: `Bearer ${token}`},
|
|
174
237
|
});
|
|
175
238
|
|
|
176
239
|
expect(res.statusCode).toBe(200);
|
|
177
|
-
expect(
|
|
178
|
-
|
|
179
|
-
|
|
240
|
+
expect(res.json()).toEqual({
|
|
241
|
+
repository_url: 'https://example.com/acme/repo.git',
|
|
242
|
+
ref: 'trunk',
|
|
243
|
+
fetch_depth: 1,
|
|
244
|
+
});
|
|
180
245
|
});
|
|
181
246
|
|
|
182
|
-
test('
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const jobB = await jobFactory.create({}, {transient: {projectId: projectB.id}});
|
|
194
|
-
createCheckoutSpec.mockResolvedValue(githubSpec('token'));
|
|
195
|
-
const token = await mintActiveLeaseToken({
|
|
196
|
-
jobId: jobA.id,
|
|
197
|
-
token: {workflowRunAttemptId: jobB.workflowRunAttemptId},
|
|
247
|
+
test('mints an explicit checkout step from its target, ref, permissions, and fetch depth', async () => {
|
|
248
|
+
const targetProjectId = crypto.randomUUID();
|
|
249
|
+
const {project, job, step} = await createRunningCheckoutStep({
|
|
250
|
+
kind: 'checkout',
|
|
251
|
+
checkout: {
|
|
252
|
+
project: targetProjectId,
|
|
253
|
+
ref: 'refs/pull/412/head',
|
|
254
|
+
fetchDepth: 0,
|
|
255
|
+
permissions: {contents: 'write'},
|
|
256
|
+
persistCredentials: false,
|
|
257
|
+
},
|
|
198
258
|
});
|
|
259
|
+
getProjectById.mockResolvedValue({project});
|
|
260
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
261
|
+
projectId: targetProjectId,
|
|
262
|
+
connectionId: crypto.randomUUID(),
|
|
263
|
+
externalRepositoryId: 'github:412',
|
|
264
|
+
});
|
|
265
|
+
createCheckoutSpec.mockResolvedValue({
|
|
266
|
+
...githubSpec('ghs-target-token'),
|
|
267
|
+
ref: 'refs/pull/412/head',
|
|
268
|
+
});
|
|
269
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
199
270
|
|
|
200
271
|
const res = await app.inject({
|
|
201
272
|
method: 'POST',
|
|
202
|
-
url:
|
|
273
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
203
274
|
headers: {authorization: `Bearer ${token}`},
|
|
204
275
|
});
|
|
205
276
|
|
|
206
277
|
expect(res.statusCode).toBe(200);
|
|
207
|
-
expect(
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
expect.objectContaining({connectionId: projectB.sourceConnectionId}),
|
|
278
|
+
expect(res.json()).toMatchObject({ref: 'refs/pull/412/head', fetch_depth: 0});
|
|
279
|
+
expect(res.json().auth.persist).toBe(false);
|
|
280
|
+
expect(resolveCheckoutTarget).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({target: {project: targetProjectId}}),
|
|
212
282
|
);
|
|
283
|
+
expect(createCheckoutSpec).toHaveBeenCalledWith({
|
|
284
|
+
workspaceId: project.workspaceId,
|
|
285
|
+
connectionId: expect.any(String),
|
|
286
|
+
externalRepositoryId: 'github:412',
|
|
287
|
+
ref: 'refs/pull/412/head',
|
|
288
|
+
permissions: {contents: 'write'},
|
|
289
|
+
});
|
|
213
290
|
});
|
|
214
291
|
|
|
215
|
-
test('
|
|
216
|
-
const project =
|
|
292
|
+
test('uses the job scope instead of a hostile lease workspace claim', async () => {
|
|
293
|
+
const {project, job, step} = await createRunningCheckoutStep();
|
|
217
294
|
getProjectById.mockResolvedValue({project});
|
|
218
|
-
|
|
295
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
296
|
+
projectId: project.id,
|
|
297
|
+
connectionId: project.sourceConnectionId,
|
|
298
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
299
|
+
});
|
|
219
300
|
createCheckoutSpec.mockResolvedValue(githubSpec('token'));
|
|
220
301
|
const token = await mintActiveLeaseToken({
|
|
221
302
|
jobId: job.id,
|
|
222
303
|
token: {workspaceId: crypto.randomUUID()},
|
|
223
304
|
});
|
|
224
305
|
|
|
225
|
-
await app.inject({
|
|
306
|
+
const res = await app.inject({
|
|
307
|
+
method: 'POST',
|
|
308
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
309
|
+
headers: {authorization: `Bearer ${token}`},
|
|
310
|
+
});
|
|
226
311
|
|
|
227
|
-
expect(
|
|
312
|
+
expect(res.statusCode).toBe(200);
|
|
313
|
+
expect(resolveCheckoutTarget).toHaveBeenCalledWith(
|
|
228
314
|
expect.objectContaining({workspaceId: project.workspaceId}),
|
|
229
315
|
);
|
|
230
316
|
});
|
|
231
317
|
|
|
232
|
-
test('returns 404 when the
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const job = await jobFactory.create({}, {transient: {projectId: project.id}});
|
|
236
|
-
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
318
|
+
test('returns 404 and mints nothing when the lease is inactive', async () => {
|
|
319
|
+
const {job, step} = await createRunningCheckoutStep();
|
|
320
|
+
const token = await mintLeaseToken({jobId: job.id});
|
|
237
321
|
|
|
238
322
|
const res = await app.inject({
|
|
239
323
|
method: 'POST',
|
|
240
|
-
url:
|
|
324
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
241
325
|
headers: {authorization: `Bearer ${token}`},
|
|
242
326
|
});
|
|
243
327
|
|
|
244
328
|
expect(res.statusCode).toBe(404);
|
|
245
|
-
expect(res.json().code).toBe('
|
|
329
|
+
expect(res.json().code).toBe('lease-not-active');
|
|
330
|
+
expect(resolveCheckoutTarget).not.toHaveBeenCalled();
|
|
331
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
246
332
|
});
|
|
247
333
|
|
|
248
|
-
test('
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
334
|
+
test('returns 404 when the requested step belongs to another job', async () => {
|
|
335
|
+
const first = await createRunningCheckoutStep();
|
|
336
|
+
const second = await createRunningCheckoutStep();
|
|
337
|
+
const token = await mintActiveLeaseToken({jobId: first.job.id});
|
|
338
|
+
|
|
339
|
+
const res = await app.inject({
|
|
340
|
+
method: 'POST',
|
|
341
|
+
url: checkoutUrl(second.step.id, second.step.currentAttempt),
|
|
342
|
+
headers: {authorization: `Bearer ${token}`},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(res.statusCode).toBe(404);
|
|
346
|
+
expect(res.json().code).toBe('step-not-found');
|
|
347
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('returns 409 for a stale step attempt', async () => {
|
|
351
|
+
const {job, step} = await createRunningCheckoutStep();
|
|
259
352
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
260
353
|
|
|
261
354
|
const res = await app.inject({
|
|
262
355
|
method: 'POST',
|
|
263
|
-
url:
|
|
356
|
+
url: checkoutUrl(step.id, step.currentAttempt + 1),
|
|
264
357
|
headers: {authorization: `Bearer ${token}`},
|
|
265
358
|
});
|
|
266
359
|
|
|
267
|
-
expect(res.statusCode).toBe(
|
|
268
|
-
expect(res.json().code).toBe('
|
|
269
|
-
expect(
|
|
360
|
+
expect(res.statusCode).toBe(409);
|
|
361
|
+
expect(res.json().code).toBe('step-attempt-mismatch');
|
|
362
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
270
363
|
});
|
|
271
364
|
|
|
272
|
-
test(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
365
|
+
test.each([
|
|
366
|
+
'pending',
|
|
367
|
+
'succeeded',
|
|
368
|
+
'failed',
|
|
369
|
+
'cancelled',
|
|
370
|
+
'skipped',
|
|
371
|
+
] as const)('returns 409 when the checkout step is %s', async (status) => {
|
|
372
|
+
const {job, step} = await createRunningCheckoutStep({status});
|
|
373
|
+
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
374
|
+
|
|
375
|
+
const res = await app.inject({
|
|
376
|
+
method: 'POST',
|
|
377
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
378
|
+
headers: {authorization: `Bearer ${token}`},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(res.statusCode).toBe(409);
|
|
382
|
+
expect(res.json().code).toBe('step-not-running');
|
|
383
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('returns 409 when the leased step is not a checkout step', async () => {
|
|
387
|
+
const {job, step} = await createRunningCheckoutStep({kind: 'run'});
|
|
283
388
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
284
389
|
|
|
285
390
|
const res = await app.inject({
|
|
286
391
|
method: 'POST',
|
|
287
|
-
url:
|
|
392
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
288
393
|
headers: {authorization: `Bearer ${token}`},
|
|
289
394
|
});
|
|
290
395
|
|
|
291
|
-
expect(res.statusCode).toBe(
|
|
396
|
+
expect(res.statusCode).toBe(409);
|
|
397
|
+
expect(res.json().code).toBe('step-not-checkout');
|
|
398
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
292
399
|
});
|
|
293
400
|
|
|
294
|
-
test('
|
|
295
|
-
const project =
|
|
401
|
+
test('refuses a target outside the workspace while the lease remains active', async () => {
|
|
402
|
+
const {project, job, step} = await createRunningCheckoutStep({
|
|
403
|
+
kind: 'checkout',
|
|
404
|
+
checkout: {project: crypto.randomUUID()},
|
|
405
|
+
});
|
|
296
406
|
getProjectById.mockResolvedValue({project});
|
|
297
|
-
|
|
298
|
-
createCheckoutSpec.mockRejectedValue(
|
|
407
|
+
resolveCheckoutTarget.mockRejectedValue(
|
|
299
408
|
createInterModuleKnownError(
|
|
300
|
-
|
|
301
|
-
'checkout-
|
|
302
|
-
{
|
|
409
|
+
projectsInterModuleContract.methods.resolveCheckoutTarget,
|
|
410
|
+
'checkout-repository-not-authorized',
|
|
411
|
+
{},
|
|
303
412
|
),
|
|
304
413
|
);
|
|
305
414
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
306
415
|
|
|
307
416
|
const res = await app.inject({
|
|
308
417
|
method: 'POST',
|
|
309
|
-
url:
|
|
418
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
310
419
|
headers: {authorization: `Bearer ${token}`},
|
|
311
420
|
});
|
|
312
421
|
|
|
313
|
-
expect(res.statusCode).toBe(
|
|
314
|
-
expect(res.json().code).toBe('
|
|
422
|
+
expect(res.statusCode).toBe(404);
|
|
423
|
+
expect(res.json().code).toBe('checkout-repository-not-authorized');
|
|
424
|
+
expect(createCheckoutSpec).not.toHaveBeenCalled();
|
|
315
425
|
});
|
|
316
426
|
|
|
317
|
-
test('
|
|
318
|
-
const project =
|
|
427
|
+
test('maps provider rate limiting to 429 without leaking credentials', async () => {
|
|
428
|
+
const {project, job, step} = await createRunningCheckoutStep();
|
|
319
429
|
getProjectById.mockResolvedValue({project});
|
|
320
|
-
|
|
321
|
-
|
|
430
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
431
|
+
projectId: project.id,
|
|
432
|
+
connectionId: project.sourceConnectionId,
|
|
433
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
434
|
+
});
|
|
435
|
+
createCheckoutSpec.mockRejectedValue(
|
|
436
|
+
createInterModuleKnownError(
|
|
437
|
+
integrationsInterModuleContract.methods.createCheckoutSpec,
|
|
438
|
+
'provider-failure',
|
|
439
|
+
{reason: 'rate-limited', retryAfterSeconds: 60},
|
|
440
|
+
),
|
|
441
|
+
);
|
|
322
442
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
323
443
|
|
|
324
444
|
const res = await app.inject({
|
|
325
445
|
method: 'POST',
|
|
326
|
-
url:
|
|
446
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
327
447
|
headers: {authorization: `Bearer ${token}`},
|
|
328
448
|
});
|
|
329
449
|
|
|
330
|
-
expect(res.statusCode).toBe(
|
|
331
|
-
expect(res.json().code).toBe('
|
|
450
|
+
expect(res.statusCode).toBe(429);
|
|
451
|
+
expect(res.json().code).toBe('rate-limited');
|
|
452
|
+
expect(res.json().details.retry_after_seconds).toBe(60);
|
|
332
453
|
});
|
|
333
454
|
|
|
334
455
|
test('never writes the minted token to a log line', async () => {
|
|
335
|
-
const project =
|
|
456
|
+
const {project, job, step} = await createRunningCheckoutStep();
|
|
336
457
|
getProjectById.mockResolvedValue({project});
|
|
337
|
-
|
|
458
|
+
resolveCheckoutTarget.mockResolvedValue({
|
|
459
|
+
projectId: project.id,
|
|
460
|
+
connectionId: project.sourceConnectionId,
|
|
461
|
+
externalRepositoryId: project.sourceExternalRepositoryId,
|
|
462
|
+
});
|
|
338
463
|
const secret = 'ghs-super-secret-token-value';
|
|
339
464
|
createCheckoutSpec.mockResolvedValue(githubSpec(secret));
|
|
340
465
|
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
341
466
|
|
|
342
467
|
const res = await app.inject({
|
|
343
468
|
method: 'POST',
|
|
344
|
-
url:
|
|
469
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
345
470
|
headers: {authorization: `Bearer ${token}`},
|
|
346
471
|
});
|
|
347
472
|
|
|
@@ -349,4 +474,94 @@ describe('POST /runs/jobs/current/checkout-token', () => {
|
|
|
349
474
|
expect(res.json().auth.token).toBe(secret);
|
|
350
475
|
expect(logLines.join('\n')).not.toContain(secret);
|
|
351
476
|
});
|
|
477
|
+
|
|
478
|
+
test('uses the active lease identity when a newer lease has replaced it', async () => {
|
|
479
|
+
const {run, project, job, step} = await createRunningCheckoutStep();
|
|
480
|
+
await insertRunningJobLease({
|
|
481
|
+
workspaceId: project.workspaceId,
|
|
482
|
+
workflowRunId: run.id,
|
|
483
|
+
workflowRunAttemptId: job.workflowRunAttemptId,
|
|
484
|
+
jobId: job.id,
|
|
485
|
+
jobExecutionId: step.jobExecutionId,
|
|
486
|
+
projectId: project.id,
|
|
487
|
+
runnerSessionId: crypto.randomUUID(),
|
|
488
|
+
});
|
|
489
|
+
const token = await mintLeaseToken({
|
|
490
|
+
jobId: job.id,
|
|
491
|
+
jobExecutionId: step.jobExecutionId,
|
|
492
|
+
runnerSessionId: crypto.randomUUID(),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const res = await app.inject({
|
|
496
|
+
method: 'POST',
|
|
497
|
+
url: checkoutUrl(step.id, step.currentAttempt),
|
|
498
|
+
headers: {authorization: `Bearer ${token}`},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(res.statusCode).toBe(404);
|
|
502
|
+
expect(res.json().code).toBe('lease-not-active');
|
|
503
|
+
});
|
|
352
504
|
});
|
|
505
|
+
|
|
506
|
+
function checkoutUrl(stepId: string, attempt: number): string {
|
|
507
|
+
return `/runs/jobs/current/steps/${stepId}/checkout-token?attempt=${attempt}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
type CheckoutConfig = {
|
|
511
|
+
project?: string;
|
|
512
|
+
connection?: string;
|
|
513
|
+
repository?: string;
|
|
514
|
+
ref?: string;
|
|
515
|
+
fetchDepth?: number;
|
|
516
|
+
permissions?: {contents: 'read' | 'write'};
|
|
517
|
+
persistCredentials?: boolean;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
async function createRunningCheckoutStep(
|
|
521
|
+
options: {
|
|
522
|
+
kind?: 'setup' | 'checkout' | 'run';
|
|
523
|
+
checkout?: CheckoutConfig;
|
|
524
|
+
status?: StepStatus;
|
|
525
|
+
} = {},
|
|
526
|
+
) {
|
|
527
|
+
const project = projectFactory.build();
|
|
528
|
+
const checkout = {
|
|
529
|
+
...(options.checkout ?? {}),
|
|
530
|
+
fetchDepth: options.checkout?.fetchDepth ?? 1,
|
|
531
|
+
permissions: options.checkout?.permissions ?? {contents: 'read'},
|
|
532
|
+
persistCredentials: options.checkout?.persistCredentials ?? true,
|
|
533
|
+
} satisfies NonNullable<
|
|
534
|
+
Extract<WorkflowModel['jobs'][number]['steps'][number], {kind: 'checkout'}>['checkout']
|
|
535
|
+
>;
|
|
536
|
+
const steps = options.kind === 'checkout' ? [{checkout}] : [{run: 'echo hello'}];
|
|
537
|
+
const run = await createWorkflowRun({
|
|
538
|
+
workspaceId: project.workspaceId,
|
|
539
|
+
projectId: project.id,
|
|
540
|
+
definitionId: crypto.randomUUID(),
|
|
541
|
+
model: workflowModel({jobs: {build: {steps}}}),
|
|
542
|
+
triggerPayload: {
|
|
543
|
+
source: 'manual',
|
|
544
|
+
event: 'fire',
|
|
545
|
+
subscriptionId: crypto.randomUUID(),
|
|
546
|
+
userId: crypto.randomUUID(),
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
const [job] = await getJobsByWorkflowRunId(run.id);
|
|
550
|
+
if (!job) throw new Error('Expected workflow job');
|
|
551
|
+
await db().update(jobsTable).set({status: 'running'}).where(eq(jobsTable.id, job.id));
|
|
552
|
+
|
|
553
|
+
const stepRows = await getStepsByJobId(job.id);
|
|
554
|
+
const targetType =
|
|
555
|
+
options.kind === 'checkout' ? 'checkout' : options.kind === 'run' ? 'run' : 'setup';
|
|
556
|
+
const step = stepRows.find((candidate) => candidate.type === targetType);
|
|
557
|
+
if (!step) throw new Error(`Expected ${targetType} step`);
|
|
558
|
+
const status = options.status ?? 'running';
|
|
559
|
+
await db().update(stepsTable).set({status}).where(eq(stepsTable.id, step.id));
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
run,
|
|
563
|
+
project,
|
|
564
|
+
job: {...job, status: 'running' as const},
|
|
565
|
+
step: {...step, status},
|
|
566
|
+
};
|
|
567
|
+
}
|