@storyshelf/app 0.6.2 → 0.7.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/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -5,13 +5,13 @@ import { credentialKeys } from "../credential-keys.mjs";
|
|
|
5
5
|
import { getStore } from "../store.mjs";
|
|
6
6
|
import { notifyProject } from "../notify.mjs";
|
|
7
7
|
import { notFound } from "./helpers.mjs";
|
|
8
|
+
import { requestBuildState } from "../git-status.mjs";
|
|
8
9
|
import { assertBaselineCurrent } from "./baseline-guard.mjs";
|
|
9
10
|
import { z } from "@hono/zod-openapi";
|
|
10
|
-
import { extractStorybookToScratch, persistStorybookStatics } from "@storyshelf/core/capture";
|
|
11
11
|
import { BaselineModel, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
|
|
12
|
-
import { BUILD_STATUSES } from "@storyshelf/core/types";
|
|
13
12
|
import { HTTPException } from "hono/http-exception";
|
|
14
13
|
import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
|
|
14
|
+
import { BUILD_STATUSES } from "@storyshelf/core/types";
|
|
15
15
|
import { Readable, Transform } from "node:stream";
|
|
16
16
|
//#region src/routers/builds.handlers.ts
|
|
17
17
|
/**
|
|
@@ -48,10 +48,12 @@ async function createBuildRecord(project, meta) {
|
|
|
48
48
|
authorName: meta.authorName,
|
|
49
49
|
message: meta.message
|
|
50
50
|
}, `/builds/${build.id}`);
|
|
51
|
+
await requestBuildState(build.id);
|
|
51
52
|
return build;
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
|
-
* Stream a raw request body into storage with a byte cap
|
|
55
|
+
* Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a
|
|
56
|
+
* chunked upload has no `Content-Length`). Resolves to the bytes stored.
|
|
55
57
|
* Throws 400 for a missing body, 413 once `maxBytes` is exceeded.
|
|
56
58
|
*/
|
|
57
59
|
async function storeUploadStream(storage, path, body, maxBytes) {
|
|
@@ -65,34 +67,12 @@ async function storeUploadStream(storage, path, body, maxBytes) {
|
|
|
65
67
|
try {
|
|
66
68
|
const nodeBody = Readable.fromWeb(body);
|
|
67
69
|
await storage.writeStream(path, nodeBody.pipe(limiter));
|
|
70
|
+
return seen;
|
|
68
71
|
} catch (error) {
|
|
69
72
|
if (seen > maxBytes) throw new HTTPException(413, { message: "Upload exceeds size limit" });
|
|
70
73
|
throw error;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Best-effort inline statics extraction for small uploads (see
|
|
75
|
-
* `maxInlineUnzipSize`). Never throws: failures are logged and the capture
|
|
76
|
-
* job backfills the same keys idempotently.
|
|
77
|
-
*/
|
|
78
|
-
async function persistInlineStatics(storage, scratchDir, maxInlineBytes, contentLength, projectId, buildId, logger) {
|
|
79
|
-
if (!scratchDir || !maxInlineBytes) return;
|
|
80
|
-
const size = parseContentLength(contentLength);
|
|
81
|
-
if (size === void 0 || size > maxInlineBytes) return;
|
|
82
|
-
try {
|
|
83
|
-
const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);
|
|
84
|
-
await persistStorybookStatics(storage, extractedDir, projectId, buildId);
|
|
85
|
-
logger.info({
|
|
86
|
-
buildId,
|
|
87
|
-
size
|
|
88
|
-
}, "upload statics persisted inline");
|
|
89
|
-
} catch (error) {
|
|
90
|
-
logger.error({
|
|
91
|
-
err: error,
|
|
92
|
-
buildId
|
|
93
|
-
}, "inline statics persist failed; capture will backfill");
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
76
|
/** Query filters accepted by the build list endpoint. */
|
|
97
77
|
const buildListQuery = z.object({
|
|
98
78
|
status: z.enum(BUILD_STATUSES).optional(),
|
|
@@ -112,34 +92,46 @@ async function snapshotForBuild(build, snapshotId) {
|
|
|
112
92
|
if (!snapshot || snapshot.buildId !== build.id) notFound("Snapshot not found");
|
|
113
93
|
return snapshot;
|
|
114
94
|
}
|
|
95
|
+
/** Roll snapshot statuses up into the build's review status. */
|
|
96
|
+
function rollupStatus(snapshots) {
|
|
97
|
+
if (snapshots.some((s) => s.status === "new" || s.status === "changed")) return "reviewing";
|
|
98
|
+
return snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
|
|
99
|
+
}
|
|
115
100
|
/** Recompute a build's counts and roll its status up from its snapshots. */
|
|
116
101
|
async function refreshBuild(buildId) {
|
|
117
|
-
const { db
|
|
102
|
+
const { db } = getStore();
|
|
118
103
|
await new BuildModel(db).updateCounts(buildId);
|
|
119
104
|
const snapshots = await new SnapshotModel(db).listByBuild(buildId);
|
|
120
|
-
const
|
|
121
|
-
let status;
|
|
122
|
-
if (unresolved) status = "reviewing";
|
|
123
|
-
else status = snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
|
|
105
|
+
const status = rollupStatus(snapshots);
|
|
124
106
|
const build = await new BuildModel(db).get(buildId);
|
|
125
|
-
if (build)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
if (!build || build.status === "cancelled") return;
|
|
108
|
+
await new BuildModel(db).setStatus(buildId, status);
|
|
109
|
+
await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the
|
|
113
|
+
* project's notification channels. Every route that moves a build to a decided status ends here.
|
|
114
|
+
*/
|
|
115
|
+
async function announceBuildStatus(buildId, status, extra = {}) {
|
|
116
|
+
const { db, config } = getStore();
|
|
117
|
+
const build = await new BuildModel(db).get(buildId);
|
|
118
|
+
if (!build) return;
|
|
119
|
+
await requestBuildState(buildId);
|
|
120
|
+
const payload = {
|
|
121
|
+
buildId,
|
|
122
|
+
status,
|
|
123
|
+
...extra
|
|
124
|
+
};
|
|
125
|
+
await emitWebhookEvent(db, { webhooks: getStore().db.tables.webhooks }, build.projectId, `build:${status}`, payload, credentialKeys(config));
|
|
126
|
+
const project = await new ProjectModel(db).get(build.projectId);
|
|
127
|
+
if (!project) return;
|
|
128
|
+
await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);
|
|
139
129
|
}
|
|
140
130
|
/**
|
|
141
131
|
* Approve a snapshot, promote its screenshot to baseline, and refresh the build.
|
|
142
132
|
* Refuses with 409 when the baseline changed since the diff, unless `force` is set.
|
|
133
|
+
* Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's
|
|
134
|
+
* outcome (and its git status) is announced a single time.
|
|
143
135
|
*/
|
|
144
136
|
async function approveSnapshot(snapshotId, userId, options = {}) {
|
|
145
137
|
const { db, config } = getStore();
|
|
@@ -149,19 +141,24 @@ async function approveSnapshot(snapshotId, userId, options = {}) {
|
|
|
149
141
|
const project = await new ProjectModel(db).get(snapshot.projectId);
|
|
150
142
|
const build = await new BuildModel(db).get(snapshot.buildId);
|
|
151
143
|
if (!project || !build) notFound("Project or build not found");
|
|
144
|
+
if (build.status === "cancelled") throw new HTTPException(409, { message: "The build was cancelled; retry it to review it" });
|
|
152
145
|
await assertBaselineCurrent(project, build, snapshot, options.force ?? false);
|
|
153
146
|
await snapshots.review(snapshotId, "approved", userId);
|
|
154
147
|
const baselines = new BaselineModel(db, void 0, getStore().storage, credentialKeys(config));
|
|
155
|
-
const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch);
|
|
156
|
-
await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath
|
|
148
|
+
const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.browser);
|
|
149
|
+
await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath, void 0, {
|
|
150
|
+
browser: snapshot.browser,
|
|
151
|
+
mode: snapshot.mode
|
|
152
|
+
});
|
|
157
153
|
await notifyProject(project, prior ? "baseline:updated" : "baseline:created", {
|
|
158
154
|
storyId: snapshot.storyId,
|
|
159
155
|
viewport: snapshot.viewportName,
|
|
156
|
+
browser: snapshot.browser ?? "chromium",
|
|
160
157
|
branch: build.gitBranch,
|
|
161
158
|
snapshotId: snapshot.id,
|
|
162
159
|
buildId: build.id
|
|
163
160
|
}, `/builds/${build.id}`);
|
|
164
|
-
await refreshBuild(build.id);
|
|
161
|
+
if (!options.deferRefresh) await refreshBuild(build.id);
|
|
165
162
|
}
|
|
166
163
|
/** Roles permitted to view builds, snapshots, and comments. */
|
|
167
164
|
const VIEW_ROLES = [
|
|
@@ -205,13 +202,7 @@ async function attachLabels(db, projectId, buildId, labels) {
|
|
|
205
202
|
await models.attach(projectId, buildId, key, value);
|
|
206
203
|
}
|
|
207
204
|
}
|
|
208
|
-
/** Parse a Content-Length header value into bytes, or undefined when unknown. */
|
|
209
|
-
function parseContentLength(value) {
|
|
210
|
-
if (!value) return;
|
|
211
|
-
const parsed = Math.trunc(Number(value));
|
|
212
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
213
|
-
}
|
|
214
205
|
//#endregion
|
|
215
|
-
export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord,
|
|
206
|
+
export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, announceBuildStatus, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, refreshBuild, snapshotForBuild, storeUploadStream };
|
|
216
207
|
|
|
217
208
|
//# sourceMappingURL=builds.handlers.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"@storyshelf/core/capture\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BaselineModel } from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<void> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/**\n * Best-effort inline statics extraction for small uploads (see\n * `maxInlineUnzipSize`). Never throws: failures are logged and the capture\n * job backfills the same keys idempotently.\n */\nexport async function persistInlineStatics(\n storage: StorageAdapter,\n scratchDir: string | undefined,\n maxInlineBytes: number | undefined,\n contentLength: string | undefined,\n projectId: string,\n buildId: string,\n logger: Logger,\n): Promise<void> {\n if (!scratchDir || !maxInlineBytes) {\n return;\n }\n const size = parseContentLength(contentLength);\n if (size === undefined || size > maxInlineBytes) {\n return;\n }\n try {\n const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);\n await persistStorybookStatics(storage, extractedDir, projectId, buildId);\n logger.info({ buildId, size }, \"upload statics persisted inline\");\n } catch (error) {\n logger.error({ err: error, buildId }, \"inline statics persist failed; capture will backfill\");\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db, config } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n {\n buildId,\n status,\n snapshotCount: snapshots.length,\n },\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (project) {\n await notifyProject(\n project,\n `build:${status}`,\n { buildId, status, snapshotCount: snapshots.length },\n `/builds/${buildId}`,\n );\n }\n }\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n );\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n );\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n await refreshBuild(build.id);\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n\n/** Parse a Content-Length header value into bytes, or undefined when unknown. */\nfunction parseContentLength(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Math.trunc(Number(value));\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport { BaselineModel } from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type BuildStatus, type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { requestBuildState } from \"../git-status.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n await requestBuildState(build.id);\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a\n * chunked upload has no `Content-Length`). Resolves to the bytes stored.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<number> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n return seen;\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Roll snapshot statuses up into the build's review status. */\nfunction rollupStatus(snapshots: Snapshot[]): \"reviewing\" | \"rejected\" | \"approved\" {\n if (snapshots.some((s) => s.status === \"new\" || s.status === \"changed\")) {\n return \"reviewing\";\n }\n return snapshots.some((s) => s.status === \"rejected\") ? \"rejected\" : \"approved\";\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const status = rollupStatus(snapshots);\n const build = await new BuildModel(db).get(buildId);\n // A cancelled build is terminal: its snapshots may still be touched, but no verdict is announced.\n if (!build || build.status === \"cancelled\") return;\n await new BuildModel(db).setStatus(buildId, status);\n await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });\n}\n\n/**\n * Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the\n * project's notification channels. Every route that moves a build to a decided status ends here.\n */\nexport async function announceBuildStatus(\n buildId: string,\n status: BuildStatus,\n extra: { snapshotCount?: number } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) return;\n await requestBuildState(buildId);\n const payload = { buildId, status, ...extra };\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n payload,\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) return;\n await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n * Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's\n * outcome (and its git status) is announced a single time.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean; deferRefresh?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n if (build.status === \"cancelled\") {\n throw new HTTPException(409, { message: \"The build was cancelled; retry it to review it\" });\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.browser,\n );\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n undefined,\n { browser: snapshot.browser, mode: snapshot.mode },\n );\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n browser: snapshot.browser ?? \"chromium\",\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n if (!options.deferRefresh) {\n await refreshBuild(build.id);\n }\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n"],"mappings":"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"}
|
package/dist/routers/builds.mjs
CHANGED
|
@@ -2,20 +2,33 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
5
6
|
import { hxRefresh } from "./htmx.mjs";
|
|
6
7
|
import { flashHx } from "./flash.mjs";
|
|
7
|
-
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
8
8
|
import { badRequest, buildSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
|
|
9
|
-
import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord,
|
|
9
|
+
import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, storeUploadStream } from "./builds.handlers.mjs";
|
|
10
10
|
import { registerAttempts } from "./attempts.mjs";
|
|
11
|
+
import { discardZip, planUpload, publishUpload } from "./publish-upload.mjs";
|
|
12
|
+
import { registerBuildCancel } from "./build-cancel.mjs";
|
|
13
|
+
import { registerBuildChangeset } from "./build-changeset.mjs";
|
|
14
|
+
import { registerBuildReviews } from "./build-reviews.mjs";
|
|
11
15
|
import { registerComments } from "./comments.mjs";
|
|
12
16
|
import { registerSnapshots } from "./snapshots.mjs";
|
|
13
17
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
14
|
-
import {
|
|
18
|
+
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
15
19
|
import { storybookZipPath } from "@storyshelf/core/utils";
|
|
20
|
+
import { BuildModel, PackageUsageModel } from "@storyshelf/core/models";
|
|
16
21
|
import { HTTPException } from "hono/http-exception";
|
|
17
|
-
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
18
22
|
//#region src/routers/builds.ts
|
|
23
|
+
/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */
|
|
24
|
+
async function planOrDiscard(zipBytes, projectId, buildId) {
|
|
25
|
+
try {
|
|
26
|
+
return planUpload(zipBytes);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
await discardZip(projectId, buildId);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
19
32
|
async function upsertContentRef(hash) {
|
|
20
33
|
try {
|
|
21
34
|
const contentRefs = getStore().db.tables.contentRefs;
|
|
@@ -48,14 +61,13 @@ function registerBuilds(app) {
|
|
|
48
61
|
});
|
|
49
62
|
app.openapi(createBuildRoute, async (c) => {
|
|
50
63
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...DEVELOPER_ROLES);
|
|
51
|
-
const
|
|
52
|
-
if (!parsed.success) throw new HTTPException(400, { message: parsed.error.message });
|
|
53
|
-
const build = await createBuildRecord(project, parsed.data);
|
|
64
|
+
const build = await createBuildRecord(project, c.req.valid("json"));
|
|
54
65
|
const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;
|
|
55
66
|
return c.json({
|
|
56
67
|
build,
|
|
57
68
|
uploadUrl,
|
|
58
|
-
baselineSha: build.baselineSha
|
|
69
|
+
baselineSha: build.baselineSha,
|
|
70
|
+
testing: getStore().testing
|
|
59
71
|
}, 202);
|
|
60
72
|
});
|
|
61
73
|
app.openapi(affectedRoute, async (c) => {
|
|
@@ -70,17 +82,23 @@ function registerBuilds(app) {
|
|
|
70
82
|
});
|
|
71
83
|
return c.json(updated);
|
|
72
84
|
});
|
|
85
|
+
app.openapi(usageRoute, async (c) => {
|
|
86
|
+
const { slug, buildId } = c.req.valid("param");
|
|
87
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
88
|
+
const build = await buildForProject(project.id, buildId);
|
|
89
|
+
const { usage } = c.req.valid("json");
|
|
90
|
+
const stored = await new PackageUsageModel(getStore().db).replaceForBuild(project.id, build.id, usage);
|
|
91
|
+
return c.json({ stored });
|
|
92
|
+
});
|
|
73
93
|
app.openapi(uploadZipRoute, async (c) => {
|
|
74
94
|
const { slug, buildId } = c.req.valid("param");
|
|
75
95
|
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
76
96
|
const build = await buildForProject(project.id, buildId);
|
|
77
97
|
const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
78
|
-
await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes);
|
|
98
|
+
const route = await planOrDiscard(await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes), project.id, build.id);
|
|
79
99
|
const reqId = c.get("requestId");
|
|
80
100
|
await getStore().enqueueCapture?.(build.id, reqId);
|
|
81
|
-
|
|
82
|
-
await persistInlineStatics(storage, config.scratchDir, config.maxInlineUnzipSize, c.req.header("content-length"), project.id, build.id, logger);
|
|
83
|
-
return c.json(build, 202);
|
|
101
|
+
return c.json(await publishUpload(route, build, reqId), 202);
|
|
84
102
|
});
|
|
85
103
|
app.openapi(dedupRoute, async (c) => {
|
|
86
104
|
const { slug, buildId } = c.req.valid("param");
|
|
@@ -130,6 +148,7 @@ function registerBuilds(app) {
|
|
|
130
148
|
});
|
|
131
149
|
else await getStore().db.update(contentRefs, f.hash, { lastSeenAt: now });
|
|
132
150
|
} catch {}
|
|
151
|
+
await getStore().publish?.completer.complete(build.id, { ok: true });
|
|
133
152
|
const reqId = c.get("requestId");
|
|
134
153
|
await getStore().enqueueCapture?.(build.id, reqId);
|
|
135
154
|
return c.json({ ok: true });
|
|
@@ -144,7 +163,7 @@ function registerBuilds(app) {
|
|
|
144
163
|
const { slug, buildId } = c.req.valid("param");
|
|
145
164
|
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
146
165
|
const build = await buildForProject(project.id, buildId);
|
|
147
|
-
const updated = await new BuildModel(getStore().db).
|
|
166
|
+
const updated = await new BuildModel(getStore().db).resetStatus(build.id, "pending");
|
|
148
167
|
await getStore().enqueueCapture?.(build.id, c.get("requestId"));
|
|
149
168
|
hxRefresh(c);
|
|
150
169
|
flashHx(c, "Capture queued");
|
|
@@ -159,6 +178,9 @@ function registerBuilds(app) {
|
|
|
159
178
|
});
|
|
160
179
|
registerSnapshots(app);
|
|
161
180
|
registerComments(app);
|
|
181
|
+
registerBuildReviews(app);
|
|
182
|
+
registerBuildChangeset(app);
|
|
183
|
+
registerBuildCancel(app);
|
|
162
184
|
registerAttempts(app);
|
|
163
185
|
}
|
|
164
186
|
const listBuildsRoute = createRoute({
|
|
@@ -182,7 +204,8 @@ const listBuildsRoute = createRoute({
|
|
|
182
204
|
const buildCreatedSchema = z.object({
|
|
183
205
|
build: buildSchema,
|
|
184
206
|
uploadUrl: z.string(),
|
|
185
|
-
baselineSha: z.string().nullable()
|
|
207
|
+
baselineSha: z.string().nullable(),
|
|
208
|
+
testing: z.boolean().openapi({ description: "False when the server has no `testing` option: the build is published but never captured or compared." })
|
|
186
209
|
}).openapi("BuildCreated");
|
|
187
210
|
const createBuildRoute = createRoute({
|
|
188
211
|
method: "post",
|
|
@@ -196,7 +219,7 @@ const createBuildRoute = createRoute({
|
|
|
196
219
|
responses: {
|
|
197
220
|
202: {
|
|
198
221
|
content: { "application/json": { schema: buildCreatedSchema } },
|
|
199
|
-
description: "Build created. PUT the Storybook zip to uploadUrl to queue capture."
|
|
222
|
+
description: "Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on)."
|
|
200
223
|
},
|
|
201
224
|
...badRequest,
|
|
202
225
|
...forbidden,
|
|
@@ -221,11 +244,11 @@ const uploadZipRoute = createRoute({
|
|
|
221
244
|
responses: {
|
|
222
245
|
202: {
|
|
223
246
|
content: { "application/json": { schema: buildSchema } },
|
|
224
|
-
description: "Zip stored and capture
|
|
247
|
+
description: "Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status."
|
|
225
248
|
},
|
|
226
249
|
413: {
|
|
227
250
|
content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
|
|
228
|
-
description: "Upload exceeds the configured size limit"
|
|
251
|
+
description: "Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)"
|
|
229
252
|
},
|
|
230
253
|
...badRequest,
|
|
231
254
|
...forbidden,
|
|
@@ -238,6 +261,7 @@ const dedupRoute = createRoute({
|
|
|
238
261
|
method: "post",
|
|
239
262
|
tags: ["Builds"],
|
|
240
263
|
summary: "Check which files need uploading",
|
|
264
|
+
description: "File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.",
|
|
241
265
|
path: "/api/v1/projects/{slug}/builds/{buildId}/dedup",
|
|
242
266
|
request: {
|
|
243
267
|
params: z.object({
|
|
@@ -258,6 +282,7 @@ const contentUploadRoute = createRoute({
|
|
|
258
282
|
method: "post",
|
|
259
283
|
tags: ["Builds"],
|
|
260
284
|
summary: "Upload build content",
|
|
285
|
+
description: "File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).",
|
|
261
286
|
path: "/api/v1/projects/{slug}/builds/{buildId}/content",
|
|
262
287
|
request: {
|
|
263
288
|
params: z.object({
|
|
@@ -283,6 +308,7 @@ const manifestRoute = createRoute({
|
|
|
283
308
|
method: "post",
|
|
284
309
|
tags: ["Builds"],
|
|
285
310
|
summary: "Store the build manifest",
|
|
311
|
+
description: "File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.",
|
|
286
312
|
path: "/api/v1/projects/{slug}/builds/{buildId}/manifest",
|
|
287
313
|
request: {
|
|
288
314
|
params: z.object({
|
|
@@ -326,6 +352,34 @@ const affectedRoute = createRoute({
|
|
|
326
352
|
...notFound
|
|
327
353
|
}
|
|
328
354
|
});
|
|
355
|
+
const usagePayloadSchema = z.object({ usage: z.array(z.object({
|
|
356
|
+
storyImportPath: z.string().min(1).max(1024),
|
|
357
|
+
packageName: z.string().min(1).max(214),
|
|
358
|
+
modulePath: z.string().min(1).max(2048),
|
|
359
|
+
version: z.string().max(128).nullable().optional()
|
|
360
|
+
})).max(2e4) });
|
|
361
|
+
const usageRoute = createRoute({
|
|
362
|
+
method: "post",
|
|
363
|
+
tags: ["Builds"],
|
|
364
|
+
summary: "Record the dependency packages each story uses",
|
|
365
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/usage",
|
|
366
|
+
request: {
|
|
367
|
+
params: z.object({
|
|
368
|
+
slug: z.string(),
|
|
369
|
+
buildId: z.string()
|
|
370
|
+
}),
|
|
371
|
+
body: { content: { "application/json": { schema: usagePayloadSchema } } }
|
|
372
|
+
},
|
|
373
|
+
responses: {
|
|
374
|
+
200: {
|
|
375
|
+
content: { "application/json": { schema: z.object({ stored: z.number() }) } },
|
|
376
|
+
description: "Usage recorded (replaces any earlier report for the build)"
|
|
377
|
+
},
|
|
378
|
+
...badRequest,
|
|
379
|
+
...forbidden,
|
|
380
|
+
...notFound
|
|
381
|
+
}
|
|
382
|
+
});
|
|
329
383
|
const getBuildRoute = createRoute({
|
|
330
384
|
method: "get",
|
|
331
385
|
tags: ["Builds"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n persistInlineStatics,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const parsed = buildCreateJsonSchema.safeParse(await c.req.json());\n if (!parsed.success) {\n throw new HTTPException(400, { message: parsed.error.message });\n }\n const build = await createBuildRecord(project, parsed.data);\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json({ build, uploadUrl, baselineSha: build.baselineSha }, 202);\n });\n\n app.openapi(affectedRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(uploadZipRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n const { storage, config, logger } = getStore();\n await persistInlineStatics(\n storage,\n config.scratchDir,\n config.maxInlineUnzipSize,\n c.req.header(\"content-length\"),\n project.id,\n build.id,\n logger,\n );\n return c.json(build, 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).setStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({ build: buildSchema, uploadUrl: z.string(), baselineSha: z.string().nullable() })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description: \"Build created. PUT the Storybook zip to uploadUrl to queue capture.\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Zip stored and capture queued\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description: \"Upload exceeds the configured size limit\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel, PackageUsageModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { discardZip, planUpload, publishUpload } from \"./publish-upload.ts\";\n\n/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */\nasync function planOrDiscard(\n zipBytes: number,\n projectId: string,\n buildId: string,\n): Promise<\"inline\" | \"job\"> {\n try {\n return planUpload(zipBytes);\n } catch (error) {\n await discardZip(projectId, buildId);\n throw error;\n }\n}\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerBuildCancel } from \"./build-cancel.ts\";\nimport { registerBuildChangeset } from \"./build-changeset.ts\";\nimport { registerBuildReviews } from \"./build-reviews.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const build = await createBuildRecord(project, c.req.valid(\"json\"));\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json(\n { build, uploadUrl, baselineSha: build.baselineSha, testing: getStore().testing },\n 202,\n );\n });\n\n app.openapi(affectedRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(usageRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { usage } = c.req.valid(\"json\");\n const stored = await new PackageUsageModel(getStore().db).replaceForBuild(\n project.id,\n build.id,\n usage,\n );\n return c.json({ stored });\n });\n\n app.openapi(uploadZipRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n const zipBytes = await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n const route = await planOrDiscard(zipBytes, project.id, build.id);\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(await publishUpload(route, build, reqId), 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // The files are stored content-addressed: nothing to extract, so publishing is complete.\n await getStore().publish?.completer.complete(build.id, { ok: true });\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).resetStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n registerBuildReviews(app);\n registerBuildChangeset(app);\n registerBuildCancel(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({\n build: buildSchema,\n uploadUrl: z.string(),\n baselineSha: z.string().nullable(),\n testing: z.boolean().openapi({\n description:\n \"False when the server has no `testing` option: the build is published but never captured or compared.\",\n }),\n })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description:\n \"Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on).\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description:\n \"Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status.\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description:\n \"Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n description:\n \"File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n description:\n \"File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n description:\n \"File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst usagePayloadSchema = z.object({\n usage: z\n .array(\n z.object({\n storyImportPath: z.string().min(1).max(1024),\n packageName: z.string().min(1).max(214),\n modulePath: z.string().min(1).max(2048),\n version: z.string().max(128).nullable().optional(),\n }),\n )\n .max(20_000),\n});\nconst usageRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the dependency packages each story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: usagePayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ stored: z.number() }) } },\n description: \"Usage recorded (replaces any earlier report for the build)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { modeSlug } from "@storyshelf/core/capture";
|
|
5
|
+
import { HTTPException } from "hono/http-exception";
|
|
6
|
+
//#region src/routers/capture-config.ts
|
|
7
|
+
/** Translating the capture settings (browsers, mode allowlist) into stored project columns. */
|
|
8
|
+
/**
|
|
9
|
+
* Translate the array fields into the stored JSON columns and keep the legacy
|
|
10
|
+
* `browser` column equal to the primary engine. Other fields pass through.
|
|
11
|
+
*/
|
|
12
|
+
function toProjectPatch(body) {
|
|
13
|
+
const { browsers, modesAllowlist, ...rest } = body;
|
|
14
|
+
const patch = { ...rest };
|
|
15
|
+
if (browsers !== void 0) {
|
|
16
|
+
const unique = browsers === null ? null : [...new Set(browsers)];
|
|
17
|
+
patch["browsers"] = unique ? JSON.stringify(unique) : null;
|
|
18
|
+
if (unique?.[0]) patch["browser"] = unique[0];
|
|
19
|
+
} else if (rest.browser !== void 0) patch["browsers"] = null;
|
|
20
|
+
if (modesAllowlist !== void 0) patch["modesAllowlist"] = modesAllowlist ? JSON.stringify(assertModeNames(modesAllowlist)) : null;
|
|
21
|
+
return patch;
|
|
22
|
+
}
|
|
23
|
+
/** 400 for a mode name that cannot form a baseline key; returns the deduped list. */
|
|
24
|
+
function assertModeNames(names) {
|
|
25
|
+
for (const name of names) try {
|
|
26
|
+
modeSlug(name);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new HTTPException(400, { message: `Invalid mode name "${name}"` });
|
|
29
|
+
}
|
|
30
|
+
return [...new Set(names)];
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { assertModeNames, toProjectPatch };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=capture-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-config.mjs","names":[],"sources":["../../src/routers/capture-config.ts"],"sourcesContent":["/** Translating the capture settings (browsers, mode allowlist) into stored project columns. */\nimport { modeSlug } from \"@storyshelf/core/capture\";\nimport { HTTPException } from \"hono/http-exception\";\n\n/** The capture-related fields of a project update (API body or settings form). */\nexport interface CaptureConfigInput {\n browser?: string | undefined;\n browsers?: string[] | null | undefined;\n modesAllowlist?: string[] | null | undefined;\n}\n\n/**\n * Translate the array fields into the stored JSON columns and keep the legacy\n * `browser` column equal to the primary engine. Other fields pass through.\n */\nexport function toProjectPatch(\n body: CaptureConfigInput & Record<string, unknown>,\n): Record<string, unknown> {\n const { browsers, modesAllowlist, ...rest } = body;\n const patch: Record<string, unknown> = { ...rest };\n if (browsers !== undefined) {\n const unique = browsers === null ? null : [...new Set(browsers)];\n patch[\"browsers\"] = unique ? JSON.stringify(unique) : null;\n if (unique?.[0]) patch[\"browser\"] = unique[0];\n } else if (rest.browser !== undefined) {\n patch[\"browsers\"] = null;\n }\n if (modesAllowlist !== undefined) {\n patch[\"modesAllowlist\"] = modesAllowlist\n ? JSON.stringify(assertModeNames(modesAllowlist))\n : null;\n }\n return patch;\n}\n\n/** 400 for a mode name that cannot form a baseline key; returns the deduped list. */\nexport function assertModeNames(names: string[]): string[] {\n for (const name of names) {\n try {\n modeSlug(name);\n } catch {\n throw new HTTPException(400, { message: `Invalid mode name \"${name}\"` });\n }\n }\n return [...new Set(names)];\n}\n"],"mappings":";;;;;;;;;;;AAeA,SAAgB,eACd,MACyB;CACzB,MAAM,EAAE,UAAU,gBAAgB,GAAG,SAAS;CAC9C,MAAM,QAAiC,EAAE,GAAG,KAAK;CACjD,IAAI,aAAa,KAAA,GAAW;EAC1B,MAAM,SAAS,aAAa,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;EAC/D,MAAM,cAAc,SAAS,KAAK,UAAU,MAAM,IAAI;EACtD,IAAI,SAAS,IAAI,MAAM,aAAa,OAAO;CAC7C,OAAO,IAAI,KAAK,YAAY,KAAA,GAC1B,MAAM,cAAc;CAEtB,IAAI,mBAAmB,KAAA,GACrB,MAAM,oBAAoB,iBACtB,KAAK,UAAU,gBAAgB,cAAc,CAAC,IAC9C;CAEN,OAAO;AACT;;AAGA,SAAgB,gBAAgB,OAA2B;CACzD,KAAK,MAAM,QAAQ,OACjB,IAAI;EACF,SAAS,IAAI;CACf,QAAQ;EACN,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,sBAAsB,KAAK,GAAG,CAAC;CACzE;CAEF,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAC3B"}
|