@storyshelf/core 0.3.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -31
- package/dist/adapter/capture-queue.d.mts +29 -1
- package/dist/adapter/capture-runner.d.mts +18 -2
- package/dist/adapter/capture-runner.mjs +12 -1
- package/dist/adapter/capture-runner.mjs.map +1 -0
- package/dist/adapter/database.d.mts +67 -17
- package/dist/adapter/database.mjs +22 -55
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/email-sender.d.mts +27 -0
- package/dist/adapter/email-sender.mjs +4 -0
- package/dist/adapter/metadata.d.mts +21 -15
- package/dist/adapter/notifier.d.mts +7 -0
- package/dist/adapter/notifier.mjs +6 -0
- package/dist/adapter/setup.d.mts +61 -0
- package/dist/adapter/setup.mjs +29 -0
- package/dist/adapter/setup.mjs.map +1 -0
- package/dist/adapter/webhook-events.d.mts +29 -0
- package/dist/adapter/webhook-events.mjs +83 -0
- package/dist/adapter/webhook-events.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +26 -0
- package/dist/adapters/notifier/events.mjs +66 -0
- package/dist/adapters/notifier/events.mjs.map +1 -0
- package/dist/adapters/notifier/format.d.mts +14 -0
- package/dist/adapters/notifier/format.mjs +96 -0
- package/dist/adapters/notifier/format.mjs.map +1 -0
- package/dist/adapters/notifier/provider.d.mts +32 -0
- package/dist/adapters/notifier/types.d.mts +56 -0
- package/dist/adapters/setup-runner.d.mts +23 -0
- package/dist/adapters/setup-runner.mjs +149 -0
- package/dist/adapters/setup-runner.mjs.map +1 -0
- package/dist/adapters/validate.d.mts +30 -0
- package/dist/adapters/validate.mjs +313 -0
- package/dist/adapters/validate.mjs.map +1 -0
- package/dist/auth.d.mts +94 -0
- package/dist/auth.mjs +4 -0
- package/dist/brand-packs.d.mts +16 -0
- package/dist/brand-packs.mjs +130 -0
- package/dist/brand-packs.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +51 -1
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +16 -0
- package/dist/capture/affected.mjs +64 -0
- package/dist/capture/affected.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +30 -0
- package/dist/capture/diff-record.mjs.map +1 -0
- package/dist/capture/dispatch.d.mts +36 -0
- package/dist/capture/dispatch.mjs +88 -31
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/guards.mjs +31 -0
- package/dist/capture/guards.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +41 -6
- package/dist/capture/orchestrator.mjs +151 -28
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +30 -16
- package/dist/capture/pipeline.mjs +143 -39
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/recovery.d.mts +21 -0
- package/dist/capture/recovery.mjs +83 -0
- package/dist/capture/recovery.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +35 -0
- package/dist/capture/rediff.mjs +77 -0
- package/dist/capture/rediff.mjs.map +1 -0
- package/dist/capture/sizing.d.mts +58 -0
- package/dist/capture/sizing.mjs +119 -0
- package/dist/capture/sizing.mjs.map +1 -0
- package/dist/capture/skip-checks.mjs +3 -3
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +38 -0
- package/dist/capture/statics.mjs +38 -4
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/status-fanout.mjs +7 -2
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +31 -2
- package/dist/capture/storybook.mjs +77 -2
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture.d.mts +10 -4
- package/dist/capture.mjs +7 -2
- package/dist/config.d.mts +121 -4
- package/dist/config.mjs +55 -8
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.mts +5 -56
- package/dist/index.mjs +4 -284
- package/dist/logger.d.mts +26 -17
- package/dist/logger.mjs +10 -5
- package/dist/logger.mjs.map +1 -1
- package/dist/models/baseline-status.d.mts +29 -0
- package/dist/models/baseline-status.mjs +26 -0
- package/dist/models/baseline-status.mjs.map +1 -0
- package/dist/models/baseline.d.mts +33 -0
- package/dist/models/baseline.mjs +35 -13
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.d.mts +87 -0
- package/dist/models/build.mjs +83 -35
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +40 -0
- package/dist/models/capture-attempt.mjs +85 -0
- package/dist/models/capture-attempt.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +21 -0
- package/dist/models/capture-log.mjs +44 -0
- package/dist/models/capture-log.mjs.map +1 -0
- package/dist/models/comment.d.mts +29 -0
- package/dist/models/comment.mjs +11 -8
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/content-ref.d.mts +21 -0
- package/dist/models/content-ref.mjs +49 -0
- package/dist/models/content-ref.mjs.map +1 -0
- package/dist/models/credentials.d.mts +36 -0
- package/dist/models/credentials.mjs +103 -0
- package/dist/models/credentials.mjs.map +1 -0
- package/dist/models/label.d.mts +58 -0
- package/dist/models/label.mjs +32 -27
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +34 -0
- package/dist/models/member.mjs +20 -11
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +52 -0
- package/dist/models/notification-channel.mjs +93 -0
- package/dist/models/notification-channel.mjs.map +1 -0
- package/dist/models/notification-subscription.d.mts +44 -0
- package/dist/models/notification-subscription.mjs +89 -0
- package/dist/models/notification-subscription.mjs.map +1 -0
- package/dist/models/project-group-mapping.d.mts +30 -0
- package/dist/models/project-group-mapping.mjs +46 -0
- package/dist/models/project-group-mapping.mjs.map +1 -0
- package/dist/models/project.d.mts +54 -0
- package/dist/models/project.mjs +15 -11
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.d.mts +58 -0
- package/dist/models/snapshot.mjs +13 -7
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.d.mts +38 -0
- package/dist/models/status-config.mjs +12 -11
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.d.mts +44 -0
- package/dist/models/token.mjs +18 -15
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/user.d.mts +42 -0
- package/dist/models/user.mjs +65 -0
- package/dist/models/user.mjs.map +1 -0
- package/dist/models/webhook.d.mts +50 -0
- package/dist/models/webhook.mjs +12 -10
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +22 -0
- package/dist/models.mjs +23 -0
- package/dist/paths.mjs +29 -0
- package/dist/paths.mjs.map +1 -1
- package/dist/retention.d.mts +45 -0
- package/dist/retention.mjs +145 -0
- package/dist/retention.mjs.map +1 -0
- package/dist/schema/baseline.d.mts +1 -0
- package/dist/schema/build.d.mts +8 -0
- package/dist/schema/capture-attempt.d.mts +38 -0
- package/dist/schema/comment.d.mts +1 -1
- package/dist/schema/content-ref.d.mts +13 -0
- package/dist/schema/member.d.mts +2 -0
- package/dist/schema/notification.d.mts +35 -0
- package/dist/schema/project-group-mapping.d.mts +15 -0
- package/dist/schema/project.d.mts +4 -0
- package/dist/schema/snapshot.d.mts +7 -0
- package/dist/schema/token.d.mts +2 -0
- package/dist/schema/user.d.mts +11 -1
- package/dist/schema.d.mts +9 -46
- package/dist/schema.mjs +1 -55
- package/dist/test-helpers/create-project.d.mts +15 -0
- package/dist/test-helpers/create-project.mjs +28 -0
- package/dist/test-helpers/create-project.mjs.map +1 -0
- package/dist/test-helpers/database-contract.d.mts +13 -0
- package/dist/test-helpers/database-contract.mjs +58 -0
- package/dist/test-helpers/database-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-adapters.d.mts +14 -0
- package/dist/test-helpers/fake-adapters.mjs +20 -0
- package/dist/test-helpers/fake-adapters.mjs.map +1 -0
- package/dist/test-helpers/fake-database.d.mts +11 -0
- package/dist/test-helpers/fake-database.mjs +104 -0
- package/dist/test-helpers/fake-database.mjs.map +1 -0
- package/dist/test-helpers/fake-storage.d.mts +14 -0
- package/dist/test-helpers/fake-storage.mjs +49 -0
- package/dist/test-helpers/fake-storage.mjs.map +1 -0
- package/dist/test-helpers/fake-tables.mjs +276 -0
- package/dist/test-helpers/fake-tables.mjs.map +1 -0
- package/dist/test-helpers/git-contract.d.mts +11 -0
- package/dist/test-helpers/git-contract.mjs +34 -0
- package/dist/test-helpers/git-contract.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +11 -0
- package/dist/test-helpers/notifier-contract.mjs +34 -0
- package/dist/test-helpers/notifier-contract.mjs.map +1 -0
- package/dist/test-helpers/queue-contract.d.mts +11 -0
- package/dist/test-helpers/queue-contract.mjs +64 -0
- package/dist/test-helpers/queue-contract.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.d.mts +11 -0
- package/dist/test-helpers/runner-contract.mjs +32 -0
- package/dist/test-helpers/runner-contract.mjs.map +1 -0
- package/dist/test-helpers/sql-chunks.mjs +138 -0
- package/dist/test-helpers/sql-chunks.mjs.map +1 -0
- package/dist/test-helpers/storage-contract.d.mts +11 -0
- package/dist/test-helpers/storage-contract.mjs +87 -0
- package/dist/test-helpers/storage-contract.mjs.map +1 -0
- package/dist/test-helpers.d.mts +13 -0
- package/dist/test-helpers.mjs +14 -0
- package/dist/tracing.mjs +63 -0
- package/dist/tracing.mjs.map +1 -0
- package/dist/types.d.mts +38 -5
- package/dist/types.mjs +18 -5
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +12 -1
- package/dist/urls.mjs +22 -3
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/encrypt.d.mts +48 -0
- package/dist/utils/encrypt.mjs +67 -13
- package/dist/utils/encrypt.mjs.map +1 -1
- package/dist/utils/hash.d.mts +17 -0
- package/dist/utils/hash.mjs +8 -2
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/http.d.mts +58 -0
- package/dist/utils/http.mjs +147 -0
- package/dist/utils/http.mjs.map +1 -0
- package/dist/utils/mime.d.mts +9 -0
- package/dist/utils/mime.mjs +33 -0
- package/dist/utils/mime.mjs.map +1 -0
- package/dist/utils/path-security.d.mts +12 -0
- package/dist/utils/path-security.mjs +24 -0
- package/dist/utils/path-security.mjs.map +1 -0
- package/dist/utils/redact.d.mts +17 -0
- package/dist/utils/redact.mjs +42 -0
- package/dist/utils/redact.mjs.map +1 -0
- package/dist/utils/timing.d.mts +19 -0
- package/dist/utils/timing.mjs +46 -0
- package/dist/utils/timing.mjs.map +1 -0
- package/dist/utils/ulid.d.mts +10 -0
- package/dist/utils/ulid.mjs +8 -8
- package/dist/utils/ulid.mjs.map +1 -1
- package/dist/utils.d.mts +12 -0
- package/dist/utils.mjs +13 -0
- package/package.json +69 -28
- package/dist/adapter/auth.d.mts +0 -50
- package/dist/adapter/auth.mjs +0 -10
- package/dist/adapter/auth.mjs.map +0 -1
- package/dist/adapter/init.d.mts +0 -58
- package/dist/adapter/init.mjs +0 -93
- package/dist/adapter/init.mjs.map +0 -1
- package/dist/adapters/webhook-events.mjs +0 -55
- package/dist/adapters/webhook-events.mjs.map +0 -1
- package/dist/ddl.d.mts +0 -8
- package/dist/ddl.mjs +0 -158
- package/dist/ddl.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/middleware/auth-gate.mjs +0 -36
- package/dist/middleware/auth-gate.mjs.map +0 -1
- package/dist/middleware/csrf.mjs +0 -40
- package/dist/middleware/csrf.mjs.map +0 -1
- package/dist/middleware/rate-limit.mjs +0 -39
- package/dist/middleware/rate-limit.mjs.map +0 -1
- package/dist/middleware/request-log.mjs +0 -28
- package/dist/middleware/request-log.mjs.map +0 -1
- package/dist/middleware/store-scope.mjs +0 -29
- package/dist/middleware/store-scope.mjs.map +0 -1
- package/dist/pages/build-detail.mjs +0 -339
- package/dist/pages/build-detail.mjs.map +0 -1
- package/dist/pages/build-diff-comments.mjs +0 -113
- package/dist/pages/build-diff-comments.mjs.map +0 -1
- package/dist/pages/build-diff-header.mjs +0 -163
- package/dist/pages/build-diff-header.mjs.map +0 -1
- package/dist/pages/build-diff-nav.mjs +0 -66
- package/dist/pages/build-diff-nav.mjs.map +0 -1
- package/dist/pages/build-diff-viewer.mjs +0 -197
- package/dist/pages/build-diff-viewer.mjs.map +0 -1
- package/dist/pages/build-diff.mjs +0 -90
- package/dist/pages/build-diff.mjs.map +0 -1
- package/dist/pages/compute-jobs.mjs +0 -198
- package/dist/pages/compute-jobs.mjs.map +0 -1
- package/dist/pages/label-detail.mjs +0 -228
- package/dist/pages/label-detail.mjs.map +0 -1
- package/dist/pages/login.mjs +0 -78
- package/dist/pages/login.mjs.map +0 -1
- package/dist/pages/project-builds.mjs +0 -231
- package/dist/pages/project-builds.mjs.map +0 -1
- package/dist/pages/project-create.mjs +0 -169
- package/dist/pages/project-create.mjs.map +0 -1
- package/dist/pages/project-settings.mjs +0 -102
- package/dist/pages/project-settings.mjs.map +0 -1
- package/dist/pages/projects.mjs +0 -184
- package/dist/pages/projects.mjs.map +0 -1
- package/dist/pages/root.mjs +0 -94
- package/dist/pages/root.mjs.map +0 -1
- package/dist/pages/settings-general.mjs +0 -192
- package/dist/pages/settings-general.mjs.map +0 -1
- package/dist/pages/settings-labels.mjs +0 -150
- package/dist/pages/settings-labels.mjs.map +0 -1
- package/dist/pages/settings-members.mjs +0 -125
- package/dist/pages/settings-members.mjs.map +0 -1
- package/dist/pages/settings-status.mjs +0 -152
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/pages/settings-tests.mjs +0 -149
- package/dist/pages/settings-tests.mjs.map +0 -1
- package/dist/pages/settings-tokens.mjs +0 -96
- package/dist/pages/settings-tokens.mjs.map +0 -1
- package/dist/pages/settings-webhooks.mjs +0 -150
- package/dist/pages/settings-webhooks.mjs.map +0 -1
- package/dist/pages/storybook.mjs +0 -96
- package/dist/pages/storybook.mjs.map +0 -1
- package/dist/retention/purge.mjs +0 -67
- package/dist/retention/purge.mjs.map +0 -1
- package/dist/routers/admin.mjs +0 -42
- package/dist/routers/admin.mjs.map +0 -1
- package/dist/routers/assets.mjs +0 -25
- package/dist/routers/assets.mjs.map +0 -1
- package/dist/routers/auth.mjs +0 -78
- package/dist/routers/auth.mjs.map +0 -1
- package/dist/routers/builds.handlers.mjs +0 -184
- package/dist/routers/builds.handlers.mjs.map +0 -1
- package/dist/routers/builds.mjs +0 -185
- package/dist/routers/builds.mjs.map +0 -1
- package/dist/routers/comments.mjs +0 -90
- package/dist/routers/comments.mjs.map +0 -1
- package/dist/routers/health.mjs +0 -137
- package/dist/routers/health.mjs.map +0 -1
- package/dist/routers/helpers.mjs +0 -82
- package/dist/routers/helpers.mjs.map +0 -1
- package/dist/routers/htmx.mjs +0 -20
- package/dist/routers/htmx.mjs.map +0 -1
- package/dist/routers/labels.mjs +0 -110
- package/dist/routers/labels.mjs.map +0 -1
- package/dist/routers/media.mjs +0 -55
- package/dist/routers/media.mjs.map +0 -1
- package/dist/routers/members.mjs +0 -102
- package/dist/routers/members.mjs.map +0 -1
- package/dist/routers/projects.mjs +0 -122
- package/dist/routers/projects.mjs.map +0 -1
- package/dist/routers/schemas.mjs +0 -224
- package/dist/routers/schemas.mjs.map +0 -1
- package/dist/routers/settings-general.mjs +0 -95
- package/dist/routers/settings-general.mjs.map +0 -1
- package/dist/routers/settings-labels.mjs +0 -49
- package/dist/routers/settings-labels.mjs.map +0 -1
- package/dist/routers/settings-members.mjs +0 -30
- package/dist/routers/settings-members.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -73
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/settings-tokens.mjs +0 -40
- package/dist/routers/settings-tokens.mjs.map +0 -1
- package/dist/routers/settings-webhooks.mjs +0 -63
- package/dist/routers/settings-webhooks.mjs.map +0 -1
- package/dist/routers/settings.handlers.mjs +0 -93
- package/dist/routers/settings.handlers.mjs.map +0 -1
- package/dist/routers/settings.mjs +0 -23
- package/dist/routers/settings.mjs.map +0 -1
- package/dist/routers/snapshots.mjs +0 -144
- package/dist/routers/snapshots.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -101
- package/dist/routers/status-configs.mjs.map +0 -1
- package/dist/routers/storybook.mjs +0 -120
- package/dist/routers/storybook.mjs.map +0 -1
- package/dist/routers/tokens.mjs +0 -81
- package/dist/routers/tokens.mjs.map +0 -1
- package/dist/routers/ui.mjs +0 -165
- package/dist/routers/ui.mjs.map +0 -1
- package/dist/routers/webhooks.mjs +0 -90
- package/dist/routers/webhooks.mjs.map +0 -1
- package/dist/schema/baseline.mjs +0 -22
- package/dist/schema/baseline.mjs.map +0 -1
- package/dist/schema/build.mjs +0 -29
- package/dist/schema/build.mjs.map +0 -1
- package/dist/schema/comment.mjs +0 -26
- package/dist/schema/comment.mjs.map +0 -1
- package/dist/schema/label.mjs +0 -30
- package/dist/schema/label.mjs.map +0 -1
- package/dist/schema/member.mjs +0 -19
- package/dist/schema/member.mjs.map +0 -1
- package/dist/schema/project.mjs +0 -25
- package/dist/schema/project.mjs.map +0 -1
- package/dist/schema/snapshot.mjs +0 -34
- package/dist/schema/snapshot.mjs.map +0 -1
- package/dist/schema/status-config.mjs +0 -20
- package/dist/schema/status-config.mjs.map +0 -1
- package/dist/schema/token.mjs +0 -19
- package/dist/schema/token.mjs.map +0 -1
- package/dist/schema/user.mjs +0 -19
- package/dist/schema/user.mjs.map +0 -1
- package/dist/schema/webhook.mjs +0 -20
- package/dist/schema/webhook.mjs.map +0 -1
- package/dist/schema.mjs.map +0 -1
- package/dist/store.mjs +0 -20
- package/dist/store.mjs.map +0 -1
- package/dist/ui/document.mjs +0 -348
- package/dist/ui/document.mjs.map +0 -1
- package/dist/ui/feedback.mjs +0 -24
- package/dist/ui/feedback.mjs.map +0 -1
- package/dist/ui/forms.mjs +0 -117
- package/dist/ui/forms.mjs.map +0 -1
- package/dist/ui/styles.mjs +0 -174
- package/dist/ui/styles.mjs.map +0 -1
- package/dist/ui/theme.mjs +0 -44
- package/dist/ui/theme.mjs.map +0 -1
|
@@ -2,14 +2,65 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { CaptureAttemptModel, emitAttemptLog } from "../models/capture-attempt.mjs";
|
|
5
6
|
import { ProjectModel } from "../models/project.mjs";
|
|
6
7
|
import { executeCaptureJob } from "./orchestrator.mjs";
|
|
7
|
-
import { hasApprovedBuildForSha, isAlreadyMerged } from "./skip-checks.mjs";
|
|
8
8
|
import { postStatusesForBuild } from "./status-fanout.mjs";
|
|
9
|
+
import { CaptureLogModel } from "../models/capture-log.mjs";
|
|
10
|
+
import { hasApprovedBuildForSha, isAlreadyMerged } from "./skip-checks.mjs";
|
|
9
11
|
//#region src/capture/dispatch.ts
|
|
12
|
+
/** Build the queue's job runner: post pending, apply skip rules, capture, report. */
|
|
13
|
+
function createDispatchJob(deps) {
|
|
14
|
+
return async (job) => {
|
|
15
|
+
const target = await loadJobTarget(deps, job);
|
|
16
|
+
if (!target) return;
|
|
17
|
+
const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;
|
|
18
|
+
await postPending(deps, target.project, pendingUrl, target.build.gitSha);
|
|
19
|
+
const { attempt, recordLog } = await beginAttempt(deps, target.project, target.build, job.reqId);
|
|
20
|
+
const ctx = {
|
|
21
|
+
deps,
|
|
22
|
+
build: target.build,
|
|
23
|
+
project: target.project,
|
|
24
|
+
pendingUrl,
|
|
25
|
+
attempt,
|
|
26
|
+
recordLog
|
|
27
|
+
};
|
|
28
|
+
if (await maybeSkipMerged(ctx) || await maybeSkipDuplicate(ctx)) return;
|
|
29
|
+
await runAndReport(ctx, job);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Open a new attempt row for this run and return its log recorder. */
|
|
33
|
+
async function beginAttempt(deps, project, build, reqId) {
|
|
34
|
+
const attempts = new CaptureAttemptModel(deps.db, deps.tables);
|
|
35
|
+
const logs = new CaptureLogModel(deps.db, deps.tables);
|
|
36
|
+
const attempt = await attempts.startAttempt(project.id, build.id, reqId);
|
|
37
|
+
const recordLog = async (level, message, fields) => {
|
|
38
|
+
await logs.append(project.id, build.id, attempt.id, level, message, fields);
|
|
39
|
+
};
|
|
40
|
+
await emitAttemptLog(recordLog, deps.logger, "info", "capture queued", {
|
|
41
|
+
buildId: build.id,
|
|
42
|
+
attemptNo: attempt.attemptNo
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
attempt,
|
|
46
|
+
recordLog
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Close the attempt row with its terminal status, counts, and error. */
|
|
50
|
+
async function finishAttempt(ctx, status, error, counts) {
|
|
51
|
+
const { deps, attempt } = ctx;
|
|
52
|
+
await new CaptureAttemptModel(deps.db, deps.tables).markFinished(attempt.id, status, error, counts);
|
|
53
|
+
await emitAttemptLog(ctx.recordLog, deps.logger, status === "failed" ? "error" : "info", `capture ${status}`, { attemptNo: attempt.attemptNo });
|
|
54
|
+
}
|
|
55
|
+
/** Full error text for an attempt row (message plus stack when available). */
|
|
56
|
+
function errorText(reason) {
|
|
57
|
+
if (reason instanceof Error) return reason.stack ?? reason.message;
|
|
58
|
+
return String(reason);
|
|
59
|
+
}
|
|
10
60
|
async function postPending(deps, project, url, sha) {
|
|
11
61
|
await postStatusesForBuild({
|
|
12
62
|
db: deps.db,
|
|
63
|
+
tables: deps.tables,
|
|
13
64
|
project,
|
|
14
65
|
sha,
|
|
15
66
|
status: "pending",
|
|
@@ -24,6 +75,7 @@ async function postPending(deps, project, url, sha) {
|
|
|
24
75
|
async function postTerminal(deps, project, url, sha, status) {
|
|
25
76
|
await postStatusesForBuild({
|
|
26
77
|
db: deps.db,
|
|
78
|
+
tables: deps.tables,
|
|
27
79
|
project,
|
|
28
80
|
sha,
|
|
29
81
|
status,
|
|
@@ -42,7 +94,12 @@ async function approveWithoutCapture(ctx, reason) {
|
|
|
42
94
|
sha: build.gitSha,
|
|
43
95
|
branch: build.gitBranch
|
|
44
96
|
}, reason);
|
|
45
|
-
await
|
|
97
|
+
await emitAttemptLog(ctx.recordLog, deps.logger, "info", reason, {
|
|
98
|
+
sha: build.gitSha,
|
|
99
|
+
branch: build.gitBranch
|
|
100
|
+
});
|
|
101
|
+
await finishAttempt(ctx, "completed");
|
|
102
|
+
await new BuildModel(deps.db, deps.tables).setStatus(build.id, "approved").catch(() => {});
|
|
46
103
|
await postTerminal(deps, project, pendingUrl, build.gitSha, "success").catch(() => {});
|
|
47
104
|
}
|
|
48
105
|
async function maybeSkipMerged(ctx) {
|
|
@@ -54,6 +111,7 @@ async function maybeSkipMerged(ctx) {
|
|
|
54
111
|
branch: build.gitBranch,
|
|
55
112
|
secret: deps.secret,
|
|
56
113
|
db: deps.db,
|
|
114
|
+
tables: deps.tables,
|
|
57
115
|
projectId: project.id,
|
|
58
116
|
logger: deps.logger
|
|
59
117
|
}).catch(() => false)) {
|
|
@@ -64,7 +122,7 @@ async function maybeSkipMerged(ctx) {
|
|
|
64
122
|
}
|
|
65
123
|
async function maybeSkipDuplicate(ctx) {
|
|
66
124
|
const { deps, build, project } = ctx;
|
|
67
|
-
if (await hasApprovedBuildForSha(deps.db, project.id, build.gitSha, build.id)) {
|
|
125
|
+
if (await hasApprovedBuildForSha(deps.db, deps.tables, project.id, build.gitSha, build.id)) {
|
|
68
126
|
await approveWithoutCapture(ctx, "skipping capture — duplicate sha already approved");
|
|
69
127
|
return true;
|
|
70
128
|
}
|
|
@@ -79,24 +137,38 @@ function terminalState(status) {
|
|
|
79
137
|
/** Post the terminal status for a finished capture. */
|
|
80
138
|
async function reportTerminalStatus(ctx, buildId) {
|
|
81
139
|
const { deps, build, project, pendingUrl } = ctx;
|
|
82
|
-
const terminal = terminalState((await new BuildModel(deps.db).get(buildId))?.status);
|
|
140
|
+
const terminal = terminalState((await new BuildModel(deps.db, deps.tables).get(buildId))?.status);
|
|
83
141
|
if (terminal) await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);
|
|
84
142
|
}
|
|
85
143
|
async function runAndReport(ctx, job) {
|
|
86
|
-
const { deps, build
|
|
144
|
+
const { deps, build } = ctx;
|
|
145
|
+
await new CaptureAttemptModel(deps.db, deps.tables).markRunning(ctx.attempt.id);
|
|
146
|
+
await emitAttemptLog(ctx.recordLog, deps.logger, "info", "capture started", {
|
|
147
|
+
buildId: build.id,
|
|
148
|
+
attemptNo: ctx.attempt.attemptNo
|
|
149
|
+
});
|
|
87
150
|
try {
|
|
88
|
-
await executeCaptureJob({
|
|
151
|
+
await finishAttempt(ctx, "completed", void 0, await executeCaptureJob({
|
|
89
152
|
buildId: job.buildId,
|
|
90
|
-
reqId: job.reqId
|
|
91
|
-
|
|
153
|
+
reqId: job.reqId,
|
|
154
|
+
traceparent: job.traceparent,
|
|
155
|
+
attempt: {
|
|
156
|
+
id: ctx.attempt.id,
|
|
157
|
+
attemptNo: ctx.attempt.attemptNo,
|
|
158
|
+
projectId: ctx.project.id
|
|
159
|
+
},
|
|
160
|
+
recordLog: ctx.recordLog
|
|
161
|
+
}, deps.jobOptions));
|
|
92
162
|
await reportTerminalStatus(ctx, job.buildId);
|
|
93
163
|
} catch (error) {
|
|
164
|
+
await finishAttempt(ctx, "failed", errorText(error));
|
|
94
165
|
await postStatusesForBuild({
|
|
95
166
|
db: deps.db,
|
|
96
|
-
|
|
167
|
+
tables: deps.tables,
|
|
168
|
+
project: ctx.project,
|
|
97
169
|
sha: build.gitSha,
|
|
98
170
|
status: "failure",
|
|
99
|
-
url: pendingUrl,
|
|
171
|
+
url: ctx.pendingUrl,
|
|
100
172
|
providers: deps.gitHosts,
|
|
101
173
|
secret: deps.secret,
|
|
102
174
|
logger: deps.logger
|
|
@@ -106,19 +178,21 @@ async function runAndReport(ctx, job) {
|
|
|
106
178
|
}
|
|
107
179
|
/** Load the build + project for a job, running orphan jobs directly. */
|
|
108
180
|
async function loadJobTarget(deps, job) {
|
|
109
|
-
const build = await new BuildModel(deps.db).get(job.buildId);
|
|
181
|
+
const build = await new BuildModel(deps.db, deps.tables).get(job.buildId);
|
|
110
182
|
if (!build) {
|
|
111
183
|
await executeCaptureJob({
|
|
112
184
|
buildId: job.buildId,
|
|
113
|
-
reqId: job.reqId
|
|
185
|
+
reqId: job.reqId,
|
|
186
|
+
traceparent: job.traceparent
|
|
114
187
|
}, deps.jobOptions);
|
|
115
188
|
return null;
|
|
116
189
|
}
|
|
117
|
-
const project = await new ProjectModel(deps.db).get(build.projectId);
|
|
190
|
+
const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);
|
|
118
191
|
if (!project) {
|
|
119
192
|
await executeCaptureJob({
|
|
120
193
|
buildId: job.buildId,
|
|
121
|
-
reqId: job.reqId
|
|
194
|
+
reqId: job.reqId,
|
|
195
|
+
traceparent: job.traceparent
|
|
122
196
|
}, deps.jobOptions);
|
|
123
197
|
return null;
|
|
124
198
|
}
|
|
@@ -127,23 +201,6 @@ async function loadJobTarget(deps, job) {
|
|
|
127
201
|
project
|
|
128
202
|
};
|
|
129
203
|
}
|
|
130
|
-
/** Build the queue's job runner: post pending, apply skip rules, capture, report. */
|
|
131
|
-
function createDispatchJob(deps) {
|
|
132
|
-
return async (job) => {
|
|
133
|
-
const target = await loadJobTarget(deps, job);
|
|
134
|
-
if (!target) return;
|
|
135
|
-
const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;
|
|
136
|
-
await postPending(deps, target.project, pendingUrl, target.build.gitSha);
|
|
137
|
-
const ctx = {
|
|
138
|
-
deps,
|
|
139
|
-
build: target.build,
|
|
140
|
-
project: target.project,
|
|
141
|
-
pendingUrl
|
|
142
|
-
};
|
|
143
|
-
if (await maybeSkipMerged(ctx) || await maybeSkipDuplicate(ctx)) return;
|
|
144
|
-
await runAndReport(ctx, job);
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
204
|
//#endregion
|
|
148
205
|
export { createDispatchJob };
|
|
149
206
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.mjs","names":[],"sources":["../../src/capture/dispatch.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./orchestrator.ts\";\nimport { hasApprovedBuildForSha, isAlreadyMerged } from \"./skip-checks.ts\";\nimport { postStatusesForBuild } from \"./status-fanout.ts\";\n\n/** Dependencies for building the capture queue's job runner. */\nexport interface DispatchDeps {\n db: DatabaseAdapter;\n jobOptions: CaptureJobOptions;\n gitHosts: GitHostProvider[];\n secret: string | undefined;\n logger: Logger;\n}\n\n/** A queued capture job. */\nexport interface CaptureDispatchJob {\n buildId: string;\n reqId?: string;\n}\n\ninterface JobContext {\n deps: DispatchDeps;\n build: Build;\n project: Project;\n pendingUrl: string;\n}\n\nasync function postPending(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha,\n status: \"pending\",\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post pending status\");\n });\n}\n\nasync function postTerminal(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n status: \"success\" | \"failure\",\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha,\n status,\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post terminal status\");\n });\n}\n\nasync function approveWithoutCapture(ctx: JobContext, reason: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n deps.logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, reason);\n await new BuildModel(deps.db).setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postTerminal(deps, project, pendingUrl, build.gitSha, \"success\").catch(() => {}); // Intentionally empty — fire-and-forget\n}\n\nasync function maybeSkipMerged(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n if (build.isDefault) {\n return false;\n }\n const merged = await isAlreadyMerged({\n providers: deps.gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: deps.secret,\n db: deps.db,\n projectId: project.id,\n logger: deps.logger,\n }).catch(() => false);\n if (merged) {\n await approveWithoutCapture(ctx, \"skipping capture — already merged\");\n return true;\n }\n return false;\n}\n\nasync function maybeSkipDuplicate(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n const dup = await hasApprovedBuildForSha(deps.db, project.id, build.gitSha, build.id);\n if (dup) {\n await approveWithoutCapture(ctx, \"skipping capture — duplicate sha already approved\");\n return true;\n }\n return false;\n}\n\n/** Map a terminal build status to its check-run state. */\nfunction terminalState(status: string | undefined): \"success\" | \"failure\" | null {\n if (status === \"approved\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"failure\";\n }\n return null;\n}\n\n/** Post the terminal status for a finished capture. */\nasync function reportTerminalStatus(ctx: JobContext, buildId: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n const updated = await new BuildModel(deps.db).get(buildId);\n const terminal = terminalState(updated?.status);\n if (terminal) {\n await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);\n }\n}\n\nasync function runAndReport(ctx: JobContext, job: CaptureDispatchJob): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n try {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n await reportTerminalStatus(ctx, job.buildId);\n } catch (error: unknown) {\n await postStatusesForBuild({\n db: deps.db,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: pendingUrl,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n}\n\n/** Load the build + project for a job, running orphan jobs directly. */\nasync function loadJobTarget(\n deps: DispatchDeps,\n job: CaptureDispatchJob,\n): Promise<{ build: Build; project: Project } | null> {\n const builds = new BuildModel(deps.db);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n return null;\n }\n const project = await new ProjectModel(deps.db).get(build.projectId);\n if (!project) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, deps.jobOptions);\n return null;\n }\n return { build, project };\n}\n\n/** Build the queue's job runner: post pending, apply skip rules, capture, report. */\nexport function createDispatchJob(deps: DispatchDeps): (job: CaptureDispatchJob) => Promise<void> {\n return async (job: CaptureDispatchJob): Promise<void> => {\n const target = await loadJobTarget(deps, job);\n if (!target) {\n return;\n }\n const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;\n await postPending(deps, target.project, pendingUrl, target.build.gitSha);\n const ctx: JobContext = { deps, build: target.build, project: target.project, pendingUrl };\n if ((await maybeSkipMerged(ctx)) || (await maybeSkipDuplicate(ctx))) {\n return;\n }\n await runAndReport(ctx, job);\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dispatch.mjs","names":[],"sources":["../../src/capture/dispatch.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport {\n CaptureAttemptModel,\n emitAttemptLog,\n type AttemptLogRecorder,\n type CaptureAttemptTables,\n} from \"../models/capture-attempt.ts\";\nimport { CaptureLogModel, type CaptureLogTables } from \"../models/capture-log.ts\";\nimport { ProjectModel, type ProjectTables } from \"../models/project.ts\";\nimport type { StatusConfigTables } from \"../models/status-config.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { CaptureAttempt } from \"../schema/capture-attempt.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./orchestrator.ts\";\nimport { hasApprovedBuildForSha, isAlreadyMerged } from \"./skip-checks.ts\";\nimport { postStatusesForBuild } from \"./status-fanout.ts\";\n\n/** Build the queue's job runner: post pending, apply skip rules, capture, report. */\nexport function createDispatchJob(deps: DispatchDeps): (job: CaptureDispatchJob) => Promise<void> {\n return async (job: CaptureDispatchJob): Promise<void> => {\n const target = await loadJobTarget(deps, job);\n if (!target) {\n return;\n }\n const pendingUrl = `/projects/${target.project.slug}/builds/${job.buildId}`;\n await postPending(deps, target.project, pendingUrl, target.build.gitSha);\n const { attempt, recordLog } = await beginAttempt(\n deps,\n target.project,\n target.build,\n job.reqId,\n );\n const ctx: JobContext = {\n deps,\n build: target.build,\n project: target.project,\n pendingUrl,\n attempt,\n recordLog,\n };\n if ((await maybeSkipMerged(ctx)) || (await maybeSkipDuplicate(ctx))) {\n return;\n }\n await runAndReport(ctx, job);\n };\n}\n\n/** Table handles required by dispatch. */\nexport type DispatchTables = BuildTables &\n ProjectTables &\n StatusConfigTables &\n CaptureAttemptTables &\n CaptureLogTables;\n\n/** Dependencies for building the capture queue's job runner. */\nexport interface DispatchDeps {\n db: DatabaseAdapter;\n tables: DispatchTables;\n jobOptions: CaptureJobOptions;\n gitHosts: GitHostProvider[];\n secret: SecretInput;\n logger: Logger;\n}\n\n/** A queued capture job. */\nexport interface CaptureDispatchJob {\n buildId: string;\n reqId?: string;\n /** W3C `traceparent` continuing the enqueueing request's trace. */\n traceparent?: string;\n}\n\ninterface JobContext {\n deps: DispatchDeps;\n build: Build;\n project: Project;\n pendingUrl: string;\n attempt: CaptureAttempt;\n recordLog: AttemptLogRecorder;\n}\n\n/** Open a new attempt row for this run and return its log recorder. */\nasync function beginAttempt(\n deps: DispatchDeps,\n project: Project,\n build: Build,\n reqId: string | undefined,\n): Promise<{ attempt: CaptureAttempt; recordLog: AttemptLogRecorder }> {\n const attempts = new CaptureAttemptModel(deps.db, deps.tables);\n const logs = new CaptureLogModel(deps.db, deps.tables);\n const attempt = await attempts.startAttempt(project.id, build.id, reqId);\n const recordLog: AttemptLogRecorder = async (level, message, fields) => {\n await logs.append(project.id, build.id, attempt.id, level, message, fields);\n };\n await emitAttemptLog(recordLog, deps.logger, \"info\", \"capture queued\", {\n buildId: build.id,\n attemptNo: attempt.attemptNo,\n });\n return { attempt, recordLog };\n}\n\n/** Close the attempt row with its terminal status, counts, and error. */\nasync function finishAttempt(\n ctx: JobContext,\n status: \"completed\" | \"failed\" | \"cancelled\",\n error?: string,\n counts?: { storyCount: number; failedCount: number },\n): Promise<void> {\n const { deps, attempt } = ctx;\n await new CaptureAttemptModel(deps.db, deps.tables).markFinished(\n attempt.id,\n status,\n error,\n counts,\n );\n await emitAttemptLog(\n ctx.recordLog,\n deps.logger,\n status === \"failed\" ? \"error\" : \"info\",\n `capture ${status}`,\n { attemptNo: attempt.attemptNo },\n );\n}\n\n/** Full error text for an attempt row (message plus stack when available). */\nfunction errorText(reason: unknown): string {\n if (reason instanceof Error) {\n return reason.stack ?? reason.message;\n }\n return String(reason);\n}\n\nasync function postPending(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project,\n sha,\n status: \"pending\",\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post pending status\");\n });\n}\n\nasync function postTerminal(\n deps: DispatchDeps,\n project: Project,\n url: string,\n sha: string,\n status: \"success\" | \"failure\",\n): Promise<void> {\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project,\n sha,\n status,\n url,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error }, \"failed to post terminal status\");\n });\n}\n\nasync function approveWithoutCapture(ctx: JobContext, reason: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n deps.logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, reason);\n await emitAttemptLog(ctx.recordLog, deps.logger, \"info\", reason, {\n sha: build.gitSha,\n branch: build.gitBranch,\n });\n await finishAttempt(ctx, \"completed\");\n await new BuildModel(deps.db, deps.tables).setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postTerminal(deps, project, pendingUrl, build.gitSha, \"success\").catch(() => {}); // Intentionally empty — fire-and-forget\n}\n\nasync function maybeSkipMerged(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n if (build.isDefault) {\n return false;\n }\n const merged = await isAlreadyMerged({\n providers: deps.gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: deps.secret,\n db: deps.db,\n tables: deps.tables,\n projectId: project.id,\n logger: deps.logger,\n }).catch(() => false);\n if (merged) {\n await approveWithoutCapture(ctx, \"skipping capture — already merged\");\n return true;\n }\n return false;\n}\n\nasync function maybeSkipDuplicate(ctx: JobContext): Promise<boolean> {\n const { deps, build, project } = ctx;\n const dup = await hasApprovedBuildForSha(\n deps.db,\n deps.tables,\n project.id,\n build.gitSha,\n build.id,\n );\n if (dup) {\n await approveWithoutCapture(ctx, \"skipping capture — duplicate sha already approved\");\n return true;\n }\n return false;\n}\n\n/** Map a terminal build status to its check-run state. */\nfunction terminalState(status: string | undefined): \"success\" | \"failure\" | null {\n if (status === \"approved\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"failure\";\n }\n return null;\n}\n\n/** Post the terminal status for a finished capture. */\nasync function reportTerminalStatus(ctx: JobContext, buildId: string): Promise<void> {\n const { deps, build, project, pendingUrl } = ctx;\n const updated = await new BuildModel(deps.db, deps.tables).get(buildId);\n const terminal = terminalState(updated?.status);\n if (terminal) {\n await postTerminal(deps, project, pendingUrl, build.gitSha, terminal);\n }\n}\n\nasync function runAndReport(ctx: JobContext, job: CaptureDispatchJob): Promise<void> {\n const { deps, build } = ctx;\n await new CaptureAttemptModel(deps.db, deps.tables).markRunning(ctx.attempt.id);\n await emitAttemptLog(ctx.recordLog, deps.logger, \"info\", \"capture started\", {\n buildId: build.id,\n attemptNo: ctx.attempt.attemptNo,\n });\n try {\n const summary = await executeCaptureJob(\n {\n buildId: job.buildId,\n reqId: job.reqId,\n traceparent: job.traceparent,\n attempt: {\n id: ctx.attempt.id,\n attemptNo: ctx.attempt.attemptNo,\n projectId: ctx.project.id,\n },\n recordLog: ctx.recordLog,\n },\n deps.jobOptions,\n );\n await finishAttempt(ctx, \"completed\", undefined, summary);\n await reportTerminalStatus(ctx, job.buildId);\n } catch (error: unknown) {\n await finishAttempt(ctx, \"failed\", errorText(error));\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project: ctx.project,\n sha: build.gitSha,\n status: \"failure\",\n url: ctx.pendingUrl,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n}\n\n/** Load the build + project for a job, running orphan jobs directly. */\nasync function loadJobTarget(\n deps: DispatchDeps,\n job: CaptureDispatchJob,\n): Promise<{ build: Build; project: Project } | null> {\n const builds = new BuildModel(deps.db, deps.tables);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob(\n { buildId: job.buildId, reqId: job.reqId, traceparent: job.traceparent },\n deps.jobOptions,\n );\n return null;\n }\n const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);\n if (!project) {\n await executeCaptureJob(\n { buildId: job.buildId, reqId: job.reqId, traceparent: job.traceparent },\n deps.jobOptions,\n );\n return null;\n }\n return { build, project };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/capture/guards.ts
|
|
5
|
+
/**
|
|
6
|
+
* Capture output guards: a third-party runner returning garbage (or
|
|
7
|
+
* gigabytes) must fail here, before bytes reach storage or baselines.
|
|
8
|
+
*/
|
|
9
|
+
/** Largest screenshot buffer the pipeline accepts (25 MiB). */
|
|
10
|
+
const MAX_SCREENSHOT_BYTES = 26214400;
|
|
11
|
+
/** PNG magic bytes every screenshot must start with. */
|
|
12
|
+
const PNG_MAGIC = [
|
|
13
|
+
137,
|
|
14
|
+
80,
|
|
15
|
+
78,
|
|
16
|
+
71,
|
|
17
|
+
13,
|
|
18
|
+
10,
|
|
19
|
+
26,
|
|
20
|
+
10
|
|
21
|
+
];
|
|
22
|
+
/** Throw when a runner-produced screenshot is unusable or unsafe to store. */
|
|
23
|
+
function assertScreenshotBuffer(buffer) {
|
|
24
|
+
if (buffer.length === 0) throw new Error("Screenshot is empty");
|
|
25
|
+
if (buffer.length > 26214400) throw new Error(`Screenshot exceeds ${MAX_SCREENSHOT_BYTES} bytes`);
|
|
26
|
+
if (!PNG_MAGIC.every((byte, index) => buffer[index] === byte)) throw new Error("Screenshot is not a PNG");
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { MAX_SCREENSHOT_BYTES, assertScreenshotBuffer };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=guards.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.mjs","names":[],"sources":["../../src/capture/guards.ts"],"sourcesContent":["/**\n * Capture output guards: a third-party runner returning garbage (or\n * gigabytes) must fail here, before bytes reach storage or baselines.\n */\n\n/** Largest screenshot buffer the pipeline accepts (25 MiB). */\nexport const MAX_SCREENSHOT_BYTES = 25 * 1024 * 1024;\n\n/** PNG magic bytes every screenshot must start with. */\nconst PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\n/** Throw when a runner-produced screenshot is unusable or unsafe to store. */\nexport function assertScreenshotBuffer(buffer: Buffer): void {\n if (buffer.length === 0) {\n throw new Error(\"Screenshot is empty\");\n }\n if (buffer.length > MAX_SCREENSHOT_BYTES) {\n throw new Error(`Screenshot exceeds ${MAX_SCREENSHOT_BYTES} bytes`);\n }\n const magic = PNG_MAGIC.every((byte, index) => buffer[index] === byte);\n if (!magic) {\n throw new Error(\"Screenshot is not a PNG\");\n }\n}\n"],"mappings":";;;;;;;;;AAMA,MAAa,uBAAuB;;AAGpC,MAAM,YAAY;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;;AAGjE,SAAgB,uBAAuB,QAAsB;CAC3D,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,qBAAqB;CAEvC,IAAI,OAAO,SAAA,UACT,MAAM,IAAI,MAAM,sBAAsB,qBAAqB,OAAO;CAGpE,IAAI,CADU,UAAU,OAAO,MAAM,UAAU,OAAO,WAAW,IACxD,GACP,MAAM,IAAI,MAAM,yBAAyB;AAE7C"}
|
|
@@ -4,29 +4,64 @@ import { Viewport } from "./adapter.mjs";
|
|
|
4
4
|
import { CaptureRunner } from "../adapter/capture-runner.mjs";
|
|
5
5
|
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
6
6
|
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
7
|
+
import { SecretInput } from "../utils/encrypt.mjs";
|
|
8
|
+
import { BuildTables } from "../models/build.mjs";
|
|
9
|
+
import { AttemptLogRecorder, CaptureAttemptTables } from "../models/capture-attempt.mjs";
|
|
10
|
+
import { CaptureLogTables } from "../models/capture-log.mjs";
|
|
11
|
+
import { ProjectTables } from "../models/project.mjs";
|
|
12
|
+
import { PipelineTables } from "./pipeline.mjs";
|
|
7
13
|
import { Logger } from "pino";
|
|
8
14
|
//#region src/capture/orchestrator.d.ts
|
|
15
|
+
/** Table handles required by the orchestrator. */
|
|
16
|
+
type OrchestratorTables = BuildTables & ProjectTables & PipelineTables & CaptureAttemptTables & CaptureLogTables;
|
|
9
17
|
/** Inputs for running a capture job against a Storybook build. */
|
|
10
18
|
interface CaptureJobOptions {
|
|
11
19
|
db: DatabaseAdapter;
|
|
20
|
+
tables: OrchestratorTables;
|
|
12
21
|
storage: StorageAdapter;
|
|
13
22
|
runner: CaptureRunner;
|
|
14
23
|
scratchDir: string;
|
|
15
24
|
viewports?: Viewport[];
|
|
16
25
|
logger?: Logger;
|
|
17
26
|
/** Server secret for decrypting webhook secrets at send time. */
|
|
18
|
-
secret?:
|
|
27
|
+
secret?: SecretInput;
|
|
28
|
+
/** Render budget override (tests use a short fuse). */
|
|
29
|
+
renderTimeoutMs?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Pointer to the attempt row owning this run's log history. */
|
|
32
|
+
interface CaptureAttemptRef {
|
|
33
|
+
id: string;
|
|
34
|
+
attemptNo: number;
|
|
35
|
+
projectId: string;
|
|
36
|
+
}
|
|
37
|
+
/** Inputs for running a capture job against a Storybook build. */
|
|
38
|
+
interface CaptureJobInput {
|
|
39
|
+
buildId: string;
|
|
40
|
+
reqId?: string;
|
|
41
|
+
/**
|
|
42
|
+
* W3C `traceparent` from the enqueueing request or queue payload.
|
|
43
|
+
* Continues the inbound trace instead of starting a root span.
|
|
44
|
+
*/
|
|
45
|
+
traceparent?: string;
|
|
46
|
+
/** Attempt row owning this run; when omitted no per-attempt logs are stored. */
|
|
47
|
+
attempt?: CaptureAttemptRef;
|
|
48
|
+
/** Recorder mirroring phase logs into the attempt's log history. */
|
|
49
|
+
recordLog?: AttemptLogRecorder;
|
|
19
50
|
}
|
|
20
51
|
/**
|
|
21
52
|
* Run the full capture for a build: extract, render, persist, and finalize.
|
|
22
53
|
*
|
|
54
|
+
* The run executes inside a `capture.job` span (continuing `input.traceparent`
|
|
55
|
+
* when present) with one child span per pipeline phase.
|
|
56
|
+
*
|
|
23
57
|
* @param input - Build id plus the originating request id.
|
|
24
58
|
* @param options - Adapters, scratch dir, viewports, and logger.
|
|
59
|
+
* @returns Story and blocking-failure counts for the attempt row.
|
|
25
60
|
*/
|
|
26
|
-
declare function executeCaptureJob(input: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
61
|
+
declare function executeCaptureJob(input: CaptureJobInput, options: CaptureJobOptions): Promise<{
|
|
62
|
+
storyCount: number;
|
|
63
|
+
failedCount: number;
|
|
64
|
+
}>;
|
|
30
65
|
//#endregion
|
|
31
|
-
export { CaptureJobOptions, executeCaptureJob };
|
|
66
|
+
export { CaptureAttemptRef, CaptureJobInput, CaptureJobOptions, OrchestratorTables, executeCaptureJob };
|
|
32
67
|
//# sourceMappingURL=orchestrator.d.mts.map
|
|
@@ -1,90 +1,213 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from "./adapter.mjs";
|
|
4
5
|
import { BuildModel } from "../models/build.mjs";
|
|
6
|
+
import { emitAttemptLog } from "../models/capture-attempt.mjs";
|
|
5
7
|
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
-
import {
|
|
8
|
+
import { parentContext, withSpan } from "../tracing.mjs";
|
|
9
|
+
import { roundedTimings, runWithTimings, timed } from "../utils/timing.mjs";
|
|
10
|
+
import { StorybookAdapter } from "./storybook.mjs";
|
|
11
|
+
import { partitionAffectedStories } from "./affected.mjs";
|
|
12
|
+
import { resolveViewports } from "./sizing.mjs";
|
|
7
13
|
import { persistCapture } from "./pipeline.mjs";
|
|
8
14
|
import { extractStorybookToScratch, persistStorybookStatics } from "./statics.mjs";
|
|
9
|
-
import { StorybookAdapter } from "./storybook.mjs";
|
|
10
15
|
//#region src/capture/orchestrator.ts
|
|
16
|
+
/** Default budget for one render call before the orchestrator cancels it. */
|
|
17
|
+
const RENDER_TIMEOUT_MS = 9e5;
|
|
18
|
+
/** Failure error text cap (persisted rows and logs stay bounded). */
|
|
19
|
+
const FAILURE_ERROR_MAX = 500;
|
|
20
|
+
/** Cap failure error text so one chatty runner cannot flood rows and logs. */
|
|
21
|
+
function capFailures(result) {
|
|
22
|
+
return {
|
|
23
|
+
captures: result.captures,
|
|
24
|
+
failures: result.failures.map((failure) => ({
|
|
25
|
+
...failure,
|
|
26
|
+
error: failure.error.length > FAILURE_ERROR_MAX ? failure.error.slice(0, FAILURE_ERROR_MAX) : failure.error
|
|
27
|
+
}))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Render with a timeout; cancels the runner when the budget expires. */
|
|
31
|
+
async function renderWithTimeout(runner, request, timeoutMs) {
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
let timer;
|
|
34
|
+
const timeout = new Promise((_, reject) => {
|
|
35
|
+
timer = setTimeout(() => {
|
|
36
|
+
timedOut = true;
|
|
37
|
+
reject(/* @__PURE__ */ new Error(`Render timed out after ${timeoutMs}ms`));
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
});
|
|
40
|
+
timer?.unref?.();
|
|
41
|
+
try {
|
|
42
|
+
return capFailures(await Promise.race([runner.render(request), timeout]));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (timedOut) await runner.cancel(request.buildId).catch(() => {});
|
|
45
|
+
throw error;
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
11
50
|
/**
|
|
12
51
|
* Run the full capture for a build: extract, render, persist, and finalize.
|
|
13
52
|
*
|
|
53
|
+
* The run executes inside a `capture.job` span (continuing `input.traceparent`
|
|
54
|
+
* when present) with one child span per pipeline phase.
|
|
55
|
+
*
|
|
14
56
|
* @param input - Build id plus the originating request id.
|
|
15
57
|
* @param options - Adapters, scratch dir, viewports, and logger.
|
|
58
|
+
* @returns Story and blocking-failure counts for the attempt row.
|
|
16
59
|
*/
|
|
17
60
|
async function executeCaptureJob(input, options) {
|
|
18
|
-
const builds = new BuildModel(options.db);
|
|
61
|
+
const builds = new BuildModel(options.db, options.tables);
|
|
19
62
|
const { build, project } = await loadTarget(options, input.buildId);
|
|
63
|
+
return await withSpan("capture.job", async () => {
|
|
64
|
+
const { result, timings } = await runWithTimings(async () => await runCapturePhases(input, options, builds, build, project));
|
|
65
|
+
options.logger?.child({
|
|
66
|
+
buildId: build.id,
|
|
67
|
+
...input.reqId ? { reqId: input.reqId } : {}
|
|
68
|
+
})?.info({ timings: roundedTimings(timings) }, "capture timings");
|
|
69
|
+
return result;
|
|
70
|
+
}, {
|
|
71
|
+
"storyshelf.build_id": build.id,
|
|
72
|
+
...input.reqId ? { "storyshelf.req_id": input.reqId } : {},
|
|
73
|
+
...input.attempt ? { "storyshelf.attempt_no": input.attempt.attemptNo } : {}
|
|
74
|
+
}, parentContext(input.traceparent));
|
|
75
|
+
}
|
|
76
|
+
/** Execute the phase pipeline inside the `capture.job` span. */
|
|
77
|
+
async function runCapturePhases(input, options, builds, build, project) {
|
|
20
78
|
const logger = options.logger?.child({
|
|
21
79
|
buildId: input.buildId,
|
|
22
|
-
reqId: input.reqId
|
|
80
|
+
reqId: input.reqId,
|
|
81
|
+
...input.attempt ? { attemptNo: input.attempt.attemptNo } : {}
|
|
23
82
|
});
|
|
24
83
|
await builds.setStatus(build.id, "capturing");
|
|
25
84
|
const startTime = performance.now();
|
|
26
85
|
let extractedDir;
|
|
27
86
|
try {
|
|
28
87
|
const extractStart = performance.now();
|
|
29
|
-
extractedDir = await
|
|
88
|
+
extractedDir = await timed("capture.extract", async () => await withSpan("capture.extract", async () => {
|
|
89
|
+
return await extractStorybookToScratch(options.storage, options.scratchDir, project.id, build.id);
|
|
90
|
+
}));
|
|
30
91
|
const extractDuration = performance.now() - extractStart;
|
|
31
92
|
logger?.info({ durationMs: Math.round(extractDuration) }, "storybook extracted");
|
|
93
|
+
await emitAttemptLog(input.recordLog, logger, "info", "storybook extracted", { durationMs: Math.round(extractDuration) });
|
|
94
|
+
const storybookDir = extractedDir;
|
|
32
95
|
const staticsStart = performance.now();
|
|
33
|
-
await
|
|
96
|
+
await timed("capture.persist-statics", async () => await withSpan("capture.persist-statics", async () => {
|
|
97
|
+
await persistStorybookStatics(options.storage, storybookDir, project.id, build.id);
|
|
98
|
+
}));
|
|
34
99
|
logger?.info({ durationMs: Math.round(performance.now() - staticsStart) }, "storybook statics persisted");
|
|
100
|
+
await emitAttemptLog(input.recordLog, logger, "info", "storybook statics persisted", { durationMs: Math.round(performance.now() - staticsStart) });
|
|
35
101
|
const stories = (await new StorybookAdapter().discover(extractedDir)).filter((s) => !isDisabledStory(s));
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
102
|
+
const rawViewports = project.viewports;
|
|
103
|
+
const viewports = resolveViewports(rawViewports, DEFAULT_VIEWPORTS, options.viewports);
|
|
104
|
+
const browser = project.browser;
|
|
105
|
+
const partition = await partitionAffectedStories({
|
|
106
|
+
db: options.db,
|
|
107
|
+
tables: options.tables,
|
|
108
|
+
projectId: project.id,
|
|
109
|
+
branch: build.gitBranch,
|
|
110
|
+
defaultBranch: project.gitDefaultBranch,
|
|
41
111
|
stories,
|
|
42
112
|
viewports,
|
|
43
|
-
|
|
44
|
-
executePlay: project.executePlay ?? false,
|
|
45
|
-
playTimeoutMs: project.playTimeoutMs ?? 1e4
|
|
113
|
+
affectedPaths: affectedPathsFor(build)
|
|
46
114
|
});
|
|
115
|
+
logger?.info({
|
|
116
|
+
rendered: partition.render.length,
|
|
117
|
+
inherited: partition.inherited.length,
|
|
118
|
+
total: stories.length
|
|
119
|
+
}, "affected capture partitioned");
|
|
120
|
+
await emitAttemptLog(input.recordLog, logger, "info", "affected capture partitioned", {
|
|
121
|
+
rendered: partition.render.length,
|
|
122
|
+
inherited: partition.inherited.length,
|
|
123
|
+
total: stories.length
|
|
124
|
+
});
|
|
125
|
+
const renderStart = performance.now();
|
|
126
|
+
const renderTimeoutMs = options.renderTimeoutMs ?? 9e5;
|
|
127
|
+
const result = await timed("capture.render", async () => await withSpan("capture.render", async () => {
|
|
128
|
+
if (partition.render.length === 0) return {
|
|
129
|
+
captures: [],
|
|
130
|
+
failures: []
|
|
131
|
+
};
|
|
132
|
+
return await renderWithTimeout(options.runner, {
|
|
133
|
+
buildId: build.id,
|
|
134
|
+
storybookDir,
|
|
135
|
+
stories: partition.render,
|
|
136
|
+
viewports,
|
|
137
|
+
logger,
|
|
138
|
+
executePlay: project.executePlay ?? false,
|
|
139
|
+
playTimeoutMs: project.playTimeoutMs ?? 1e4,
|
|
140
|
+
runA11y: project.runA11y ?? false,
|
|
141
|
+
browser: browser ?? "chromium"
|
|
142
|
+
}, renderTimeoutMs);
|
|
143
|
+
}, { "storyshelf.render_count": partition.render.length }));
|
|
47
144
|
const renderDuration = performance.now() - renderStart;
|
|
48
145
|
logger?.info({
|
|
49
146
|
durationMs: Math.round(renderDuration),
|
|
50
147
|
storyCount: stories.length
|
|
51
148
|
}, "stories rendered");
|
|
149
|
+
await emitAttemptLog(input.recordLog, logger, "info", "stories rendered", {
|
|
150
|
+
durationMs: Math.round(renderDuration),
|
|
151
|
+
storyCount: stories.length
|
|
152
|
+
});
|
|
52
153
|
const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));
|
|
53
154
|
const blockingFailed = /* @__PURE__ */ new Set();
|
|
54
155
|
const flakyFailed = /* @__PURE__ */ new Set();
|
|
55
|
-
|
|
156
|
+
const a11yFailed = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const f of result.failures) if (f.error.startsWith("a11y:")) a11yFailed.add(f.storyId);
|
|
158
|
+
else if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);
|
|
56
159
|
else blockingFailed.add(f.storyId);
|
|
57
160
|
const persistStart = performance.now();
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
161
|
+
await timed("capture.persist", async () => await withSpan("capture.persist", async () => {
|
|
162
|
+
await persistCapture({
|
|
163
|
+
db: options.db,
|
|
164
|
+
tables: options.tables,
|
|
165
|
+
storage: options.storage,
|
|
166
|
+
project,
|
|
167
|
+
build,
|
|
168
|
+
viewports,
|
|
169
|
+
captures: result.captures,
|
|
170
|
+
inherited: partition.inherited,
|
|
171
|
+
logger,
|
|
172
|
+
recordLog: input.recordLog,
|
|
173
|
+
secret: options.secret
|
|
174
|
+
}, blockingFailed, flakyFailed, a11yFailed);
|
|
175
|
+
}));
|
|
68
176
|
const persistDuration = performance.now() - persistStart;
|
|
69
177
|
logger?.info({ durationMs: Math.round(persistDuration) }, "capture persisted");
|
|
178
|
+
await emitAttemptLog(input.recordLog, logger, "info", "capture persisted", { durationMs: Math.round(persistDuration) });
|
|
70
179
|
const totalDuration = performance.now() - startTime;
|
|
71
180
|
logger?.info({ durationMs: Math.round(totalDuration) }, "capture completed");
|
|
181
|
+
await emitAttemptLog(input.recordLog, logger, "info", "capture completed", { durationMs: Math.round(totalDuration) });
|
|
182
|
+
return {
|
|
183
|
+
storyCount: stories.length,
|
|
184
|
+
failedCount: blockingFailed.size
|
|
185
|
+
};
|
|
72
186
|
} catch (error) {
|
|
73
187
|
const totalDuration = performance.now() - startTime;
|
|
74
188
|
logger?.error({
|
|
75
189
|
durationMs: Math.round(totalDuration),
|
|
76
190
|
err: error
|
|
77
191
|
}, "capture failed");
|
|
192
|
+
await emitAttemptLog(input.recordLog, logger, "error", "capture failed", { durationMs: Math.round(totalDuration) });
|
|
78
193
|
await builds.setStatus(build.id, "failed").catch((markError) => {
|
|
79
194
|
logger?.error({ err: markError }, "failed to mark build failed after capture error");
|
|
80
195
|
});
|
|
81
196
|
throw error;
|
|
82
197
|
}
|
|
83
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the affected import paths for a build, or `null` for full capture
|
|
201
|
+
* (affected capture disabled, or no computation was posted).
|
|
202
|
+
*/
|
|
203
|
+
function affectedPathsFor(build) {
|
|
204
|
+
if (!build.affectedOnly) return null;
|
|
205
|
+
return BuildModel.affectedPaths(build);
|
|
206
|
+
}
|
|
84
207
|
async function loadTarget(options, buildId) {
|
|
85
|
-
const build = await new BuildModel(options.db).get(buildId);
|
|
208
|
+
const build = await new BuildModel(options.db, options.tables).get(buildId);
|
|
86
209
|
if (!build) throw new Error(`Build not found: ${buildId}`);
|
|
87
|
-
const project = await new ProjectModel(options.db).get(build.projectId);
|
|
210
|
+
const project = await new ProjectModel(options.db, options.tables).get(build.projectId);
|
|
88
211
|
if (!project) throw new Error(`Project not found: ${build.projectId}`);
|
|
89
212
|
return {
|
|
90
213
|
build,
|
|
@@ -92,6 +215,6 @@ async function loadTarget(options, buildId) {
|
|
|
92
215
|
};
|
|
93
216
|
}
|
|
94
217
|
//#endregion
|
|
95
|
-
export { executeCaptureJob };
|
|
218
|
+
export { RENDER_TIMEOUT_MS, executeCaptureJob };
|
|
96
219
|
|
|
97
220
|
//# sourceMappingURL=orchestrator.mjs.map
|