@storyshelf/app 0.7.0 → 0.9.0-next-d2cc082
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/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +1 -2
- package/dist/assets/htmx.mjs +1 -2
- package/dist/assets/mark.mjs +1 -2
- package/dist/boot-tasks.mjs +1 -2
- package/dist/build-state-flags.mjs +1 -2
- package/dist/build-state-setup.mjs +1 -2
- package/dist/capture-alerts.mjs +1 -2
- package/dist/capture-process.mjs +1 -2
- package/dist/capture-setup.mjs +1 -2
- package/dist/connections.mjs +1 -2
- package/dist/consumer-setup.mjs +1 -2
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/design-events.mjs +71 -0
- package/dist/design-events.mjs.map +1 -0
- package/dist/git-status.mjs +1 -2
- package/dist/hidden-data.mjs +1 -2
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +2 -3
- package/dist/insights/budget.mjs +1 -2
- package/dist/insights/deps.mjs +1 -2
- package/dist/insights/errors.mjs +1 -2
- package/dist/insights/health-panel.mjs +0 -1
- package/dist/insights/job.mjs +1 -2
- package/dist/insights/load-health.mjs +1 -2
- package/dist/insights/load-triage.mjs +1 -2
- package/dist/insights/panel.mjs +1 -2
- package/dist/insights/request.mjs +1 -2
- package/dist/insights/stale.mjs +1 -2
- package/dist/insights/usage-report.mjs +1 -2
- package/dist/insights/view.mjs +1 -2
- package/dist/job-canceller.mjs +1 -2
- package/dist/job-queue.mjs +1 -2
- package/dist/jobs-setup.mjs +1 -2
- package/dist/lifecycle.mjs +2 -3
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +1 -2
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +1 -2
- package/dist/middleware/request-log.mjs +1 -2
- package/dist/middleware/require-review.mjs +1 -2
- package/dist/middleware/require-testing.mjs +1 -2
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/store-scope.mjs +1 -2
- package/dist/notifications-pause.mjs +1 -2
- package/dist/notify.d.mts +1 -3
- package/dist/notify.mjs +5 -3
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +1 -2
- package/dist/pages/admin-ai.mjs +1 -2
- package/dist/pages/admin-system-origin.mjs +19 -10
- package/dist/pages/admin-system-origin.mjs.map +1 -1
- package/dist/pages/admin-system-targets.mjs +1 -2
- package/dist/pages/admin-system.mjs +1 -2
- package/dist/pages/build-detail.mjs +1 -2
- package/dist/pages/build-diff-actions.mjs +1 -2
- package/dist/pages/build-diff-comments.mjs +1 -2
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +34 -5
- package/dist/pages/build-diff-environment.mjs.map +1 -1
- package/dist/pages/build-diff-header.mjs +1 -2
- package/dist/pages/build-diff-insight.mjs +1 -2
- package/dist/pages/build-diff-model.mjs +20 -3
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +1 -2
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +1 -2
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +1 -2
- package/dist/pages/build-diff-viewer.mjs +38 -12
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +6 -12
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +1 -2
- package/dist/pages/component-detail.mjs +1 -2
- package/dist/pages/component-stories.mjs +1 -2
- package/dist/pages/compute-jobs.mjs +1 -2
- package/dist/pages/design-links.mjs +1 -2
- package/dist/pages/environment-change.mjs +16 -4
- package/dist/pages/environment-change.mjs.map +1 -1
- package/dist/pages/health-panel.mjs +1 -2
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +1 -2
- package/dist/pages/library-model.mjs +1 -2
- package/dist/pages/library-snapshots.mjs +1 -2
- package/dist/pages/library.mjs +1 -2
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +1 -2
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +1 -2
- package/dist/pages/project-builds-table.mjs +1 -2
- package/dist/pages/project-builds.mjs +1 -2
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +1 -2
- package/dist/pages/projects-card.mjs +1 -2
- package/dist/pages/projects.mjs +1 -2
- package/dist/pages/review-changeset.mjs +1 -2
- package/dist/pages/review-comments.mjs +1 -2
- package/dist/pages/review-verdict.mjs +1 -2
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +1 -2
- package/dist/pages/settings-capture.mjs +1 -2
- package/dist/pages/settings-design-credential.mjs +1 -2
- package/dist/pages/settings-design-links.mjs +1 -2
- package/dist/pages/settings-design.mjs +1 -2
- package/dist/pages/settings-general.mjs +1 -2
- package/dist/pages/settings-labels.mjs +1 -2
- package/dist/pages/settings-links.mjs +1 -2
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +1 -2
- package/dist/pages/settings-notifications.mjs +1 -2
- package/dist/pages/settings-tests.mjs +1 -2
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +1 -2
- package/dist/publish-setup.mjs +1 -2
- package/dist/retention-timer.mjs +10 -11
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +1 -2
- package/dist/routers/admin-pages.mjs +1 -2
- package/dist/routers/admin-public-origin.mjs +10 -3
- package/dist/routers/admin-public-origin.mjs.map +1 -1
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -2
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +1 -2
- package/dist/routers/baseline-guard.mjs +2 -3
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +1 -2
- package/dist/routers/baselines.mjs +1 -2
- package/dist/routers/build-cancel.mjs +1 -2
- package/dist/routers/build-changeset.mjs +1 -2
- package/dist/routers/build-reviews.handlers.mjs +1 -2
- package/dist/routers/build-reviews.mjs +1 -2
- package/dist/routers/builds.handlers.mjs +23 -9
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +7 -3
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +1 -2
- package/dist/routers/comments.mjs +1 -2
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +1 -2
- package/dist/routers/component.handlers.mjs +1 -2
- package/dist/routers/connections.mjs +1 -2
- package/dist/routers/design-connect.handlers.mjs +1 -2
- package/dist/routers/design.handlers.mjs +8 -2
- package/dist/routers/design.handlers.mjs.map +1 -1
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +1 -2
- package/dist/routers/health.mjs +1 -2
- package/dist/routers/helpers.mjs +1 -2
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +1 -2
- package/dist/routers/insights.handlers.mjs +1 -2
- package/dist/routers/insights.mjs +1 -2
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +1 -2
- package/dist/routers/links.mjs +1 -2
- package/dist/routers/media.mjs +14 -5
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -2
- package/dist/routers/notification-channels.mjs +1 -2
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +1 -2
- package/dist/routers/preview-base.mjs +1 -2
- package/dist/routers/preview-token.mjs +1 -2
- package/dist/routers/profile.mjs +1 -2
- package/dist/routers/projects.mjs +1 -2
- package/dist/routers/publish-upload.mjs +19 -12
- package/dist/routers/publish-upload.mjs.map +1 -1
- package/dist/routers/published-access.mjs +1 -2
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +26 -9
- package/dist/routers/published-files.mjs.map +1 -1
- package/dist/routers/review-forms.mjs +1 -2
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +1 -2
- package/dist/routers/review-switch-off.mjs +1 -2
- package/dist/routers/schemas.mjs +1 -2
- package/dist/routers/scoped-models.mjs +1 -2
- package/dist/routers/settings-ai.mjs +1 -2
- package/dist/routers/settings-capture.mjs +1 -2
- package/dist/routers/settings-design.mjs +1 -2
- package/dist/routers/settings-general.mjs +1 -2
- package/dist/routers/settings-git-channel.mjs +1 -2
- package/dist/routers/settings-labels.mjs +1 -2
- package/dist/routers/settings-links.mjs +1 -2
- package/dist/routers/settings-members.mjs +1 -2
- package/dist/routers/settings-notifications.mjs +1 -2
- package/dist/routers/settings-tokens.mjs +1 -2
- package/dist/routers/settings-webhooks.mjs +1 -2
- package/dist/routers/settings.handlers.mjs +1 -2
- package/dist/routers/settings.mjs +1 -2
- package/dist/routers/signed-storybook.mjs +5 -3
- package/dist/routers/signed-storybook.mjs.map +1 -1
- package/dist/routers/snapshot-groups.mjs +1 -2
- package/dist/routers/snapshots.mjs +1 -2
- package/dist/routers/storybook.mjs +21 -17
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +1 -2
- package/dist/routers/ui.mjs +4 -2
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +1 -2
- package/dist/routers/well-known.mjs +1 -2
- package/dist/routes/api-routes.mjs +1 -2
- package/dist/routes/classification.mjs +1 -2
- package/dist/routes/openapi-classes.mjs +1 -2
- package/dist/routes/page-routes.mjs +2 -3
- package/dist/routes/page-routes.mjs.map +1 -1
- package/dist/runtime.mjs +1 -2
- package/dist/serverless.mjs +1 -2
- package/dist/store.mjs +1 -2
- package/dist/targets.mjs +1 -2
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +1 -2
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +1 -2
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +1 -2
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +1 -2
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +1 -2
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +1 -2
- package/dist/ui/shell/nav.mjs +1 -2
- package/dist/ui/shell/review-script.mjs +17 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +1 -2
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +9 -18
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { MemberModel, ProjectGroupMappingModel, ProjectModel, UserModel } from "@storyshelf/core/models";
|
|
5
|
-
|
|
5
|
+
// src/routers/auth-sync.ts
|
|
6
6
|
const ROLE_RANK = {
|
|
7
7
|
viewer: 0,
|
|
8
8
|
developer: 1,
|
|
@@ -58,7 +58,6 @@ async function syncProjectMembership(members, mappings, projectId, userId, group
|
|
|
58
58
|
const existing = await members.get(projectId, userId);
|
|
59
59
|
if (!existing || existing.role !== top.role || existing.source !== `oidc:${top.groupName}`) await members.set(projectId, userId, top.role, `oidc:${top.groupName}`);
|
|
60
60
|
}
|
|
61
|
-
//#endregion
|
|
62
61
|
export { syncLoginMemberships };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=auth-sync.mjs.map
|
package/dist/routers/auth.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { UserModel } from "@storyshelf/core/models";
|
|
|
11
11
|
import { HTTPException } from "hono/http-exception";
|
|
12
12
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
13
13
|
import { MIN_PASSWORD_LENGTH } from "@storyshelf/core/auth";
|
|
14
|
-
|
|
14
|
+
// src/routers/auth.ts
|
|
15
15
|
/** Provider ids are URL path segments: strict charset, bounded length. */
|
|
16
16
|
function validProviderId(value) {
|
|
17
17
|
return /^[a-zA-Z0-9_-]{1,64}$/u.test(value);
|
|
@@ -232,7 +232,6 @@ function registerEngineAuth(app, engine) {
|
|
|
232
232
|
return hxRedirect(c, "/auth/login");
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
|
-
//#endregion
|
|
236
235
|
export { forwardToEngine, registerEngineAuth };
|
|
237
236
|
|
|
238
237
|
//# sourceMappingURL=auth.mjs.map
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { BaselineModel, baselineStatus, isBaselineDrifted } from "@storyshelf/core/models";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
|
-
|
|
7
|
+
// src/routers/baseline-guard.ts
|
|
8
8
|
/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */
|
|
9
9
|
/** Whether a snapshot is awaiting a review decision. */
|
|
10
10
|
function isOpenSnapshot(snapshot) {
|
|
@@ -71,13 +71,12 @@ async function loadBaselineView(project, build, snapshots) {
|
|
|
71
71
|
drifted: {}
|
|
72
72
|
};
|
|
73
73
|
for (const { snapshot, current } of entries) {
|
|
74
|
-
view.hasBaseline[snapshot.id] = Boolean(current);
|
|
74
|
+
view.hasBaseline[snapshot.id] = Boolean(snapshot.comparisonImagePath ?? current);
|
|
75
75
|
const status = baselineStatus(snapshot, current);
|
|
76
76
|
if (!build.isDefault && isOpenSnapshot(snapshot) && (status === "stale" || status === "removed")) view.drifted[snapshot.id] = status;
|
|
77
77
|
}
|
|
78
78
|
return view;
|
|
79
79
|
}
|
|
80
|
-
//#endregion
|
|
81
80
|
export { assertBaselineCurrent, baselineChanged, findDriftedSnapshots, isBaselineChanged, isOpenSnapshot, loadBaselineView, snapshotBaselineStatus };
|
|
82
81
|
|
|
83
82
|
//# sourceMappingURL=baseline-guard.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline-guard.mjs","names":[],"sources":["../../src/routers/baseline-guard.ts"],"sourcesContent":["/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */\nimport {\n BaselineModel,\n baselineStatus,\n isBaselineDrifted,\n type BaselineStatus,\n} from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** Whether a snapshot is awaiting a review decision. */\nexport function isOpenSnapshot(snapshot: Pick<Snapshot, \"status\">): boolean {\n return snapshot.status === \"new\" || snapshot.status === \"changed\";\n}\n\n/** Compare a snapshot's recorded baseline with the baseline that applies to it now. */\nexport async function snapshotBaselineStatus(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n): Promise<BaselineStatus> {\n const baselines = new BaselineModel(getStore().db);\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return baselineStatus(snapshot, current);\n}\n\n/** Open snapshots whose diff no longer matches the current baseline. */\nexport async function findDriftedSnapshots(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshots: Snapshot[],\n): Promise<Map<string, BaselineStatus>> {\n const open = snapshots.filter((snapshot) => isOpenSnapshot(snapshot));\n const statuses = await Promise.all(\n open.map(async (snapshot) => await snapshotBaselineStatus(project, build, snapshot)),\n );\n const drifted = new Map<string, BaselineStatus>();\n for (const [index, snapshot] of open.entries()) {\n const status = statuses[index];\n if (status && isBaselineDrifted(status)) {\n drifted.set(snapshot.id, status);\n }\n }\n return drifted;\n}\n\n/** 409 telling the client the diff is out of date; recompute it or force the approval. */\nexport function baselineChanged(snapshotId: string, status: BaselineStatus): HTTPException {\n const body = {\n code: \"baseline_changed\",\n message:\n \"The baseline changed after this diff was computed. Re-diff the build, or approve with force.\",\n snapshotId,\n baselineStatus: status,\n };\n return new HTTPException(409, {\n res: new Response(JSON.stringify(body), {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n }),\n });\n}\n\n/** Throw {@link baselineChanged} when an open snapshot's baseline drifted and `force` is off. */\nexport async function assertBaselineCurrent(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n force: boolean,\n): Promise<void> {\n if (!isOpenSnapshot(snapshot)) {\n return;\n }\n const status = await snapshotBaselineStatus(project, build, snapshot);\n if (!isBaselineDrifted(status)) {\n return;\n }\n if (force) {\n getStore().logger.warn(\n { snapshotId: snapshot.id, baselineStatus: status, userId: getStore().user?.id ?? null },\n \"approved snapshot despite changed baseline (forced)\",\n );\n return;\n }\n throw baselineChanged(snapshot.id, status);\n}\n\n/** True when `error` is the 409 raised by {@link baselineChanged}. */\nexport function isBaselineChanged(error: unknown): boolean {\n return error instanceof HTTPException && error.status === 409;\n}\n\n/** Per-snapshot baseline facts the review pages need. */\nexport interface BaselineView {\n /** Snapshot ids that have a baseline to compare against. */\n hasBaseline: Record<string, boolean>;\n /** Open snapshots whose recorded baseline no longer matches the current one. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** Resolve each snapshot's current baseline once and derive presence plus drift. */\nexport async function loadBaselineView(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\" | \"isDefault\">,\n snapshots: Snapshot[],\n): Promise<BaselineView> {\n const baselines = new BaselineModel(getStore().db);\n const entries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return { snapshot, current };\n }),\n );\n const view: BaselineView = { hasBaseline: {}, drifted: {} };\n for (const { snapshot, current } of entries) {\n view.hasBaseline[snapshot.id] = Boolean(current);\n const status = baselineStatus(snapshot, current);\n if (\n !build.isDefault &&\n isOpenSnapshot(snapshot) &&\n (status === \"stale\" || status === \"removed\")\n ) {\n view.drifted[snapshot.id] = status;\n }\n }\n return view;\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,eAAe,UAA6C;CAC1E,OAAO,SAAS,WAAW,SAAS,SAAS,WAAW;AAC1D;;AAGA,eAAsB,uBACpB,SACA,OACA,UACyB;CAEzB,MAAM,UAAU,MAAM,IADA,cAAc,SAAS,CAAC,CAAC,EACjB,CAAC,CAAC,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,OAAO,eAAe,UAAU,OAAO;AACzC;;AAGA,eAAsB,qBACpB,SACA,OACA,WACsC;CACtC,MAAM,OAAO,UAAU,QAAQ,aAAa,eAAe,QAAQ,CAAC;CACpE,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,aAAa,MAAM,uBAAuB,SAAS,OAAO,QAAQ,CAAC,CACrF;CACA,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,CAAC,OAAO,aAAa,KAAK,QAAQ,GAAG;EAC9C,MAAM,SAAS,SAAS;EACxB,IAAI,UAAU,kBAAkB,MAAM,GACpC,QAAQ,IAAI,SAAS,IAAI,MAAM;CAEnC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,YAAoB,QAAuC;CAQzF,OAAO,IAAI,cAAc,KAAK,EAC5B,KAAK,IAAI,SAAS,KAAK,UAAU;EAPjC,MAAM;EACN,SACE;EACF;EACA,gBAAgB;CAGoB,CAAC,GAAG;EACtC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC,EACH,CAAC;AACH;;AAGA,eAAsB,sBACpB,SACA,OACA,UACA,OACe;CACf,IAAI,CAAC,eAAe,QAAQ,GAC1B;CAEF,MAAM,SAAS,MAAM,uBAAuB,SAAS,OAAO,QAAQ;CACpE,IAAI,CAAC,kBAAkB,MAAM,GAC3B;CAEF,IAAI,OAAO;EACT,SAAS,CAAC,CAAC,OAAO,KAChB;GAAE,YAAY,SAAS;GAAI,gBAAgB;GAAQ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;EAAK,GACvF,qDACF;EACA;CACF;CACA,MAAM,gBAAgB,SAAS,IAAI,MAAM;AAC3C;;AAGA,SAAgB,kBAAkB,OAAyB;CACzD,OAAO,iBAAiB,iBAAiB,MAAM,WAAW;AAC5D;;AAWA,eAAsB,iBACpB,SACA,OACA,WACuB;CACvB,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,UAAU,MAAM,QAAQ,IAC5B,UAAU,IAAI,OAAO,aAAa;EAShC,OAAO;GAAE;GAAU,SAAA,MARG,UAAU,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;EAC2B;CAC7B,CAAC,CACH;CACA,MAAM,OAAqB;EAAE,aAAa,CAAC;EAAG,SAAS,CAAC;CAAE;CAC1D,KAAK,MAAM,EAAE,UAAU,aAAa,SAAS;EAC3C,KAAK,YAAY,SAAS,MAAM,QAAQ,OAAO;EAC/
|
|
1
|
+
{"version":3,"file":"baseline-guard.mjs","names":[],"sources":["../../src/routers/baseline-guard.ts"],"sourcesContent":["/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */\nimport {\n BaselineModel,\n baselineStatus,\n isBaselineDrifted,\n type BaselineStatus,\n} from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** Whether a snapshot is awaiting a review decision. */\nexport function isOpenSnapshot(snapshot: Pick<Snapshot, \"status\">): boolean {\n return snapshot.status === \"new\" || snapshot.status === \"changed\";\n}\n\n/** Compare a snapshot's recorded baseline with the baseline that applies to it now. */\nexport async function snapshotBaselineStatus(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n): Promise<BaselineStatus> {\n const baselines = new BaselineModel(getStore().db);\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return baselineStatus(snapshot, current);\n}\n\n/** Open snapshots whose diff no longer matches the current baseline. */\nexport async function findDriftedSnapshots(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshots: Snapshot[],\n): Promise<Map<string, BaselineStatus>> {\n const open = snapshots.filter((snapshot) => isOpenSnapshot(snapshot));\n const statuses = await Promise.all(\n open.map(async (snapshot) => await snapshotBaselineStatus(project, build, snapshot)),\n );\n const drifted = new Map<string, BaselineStatus>();\n for (const [index, snapshot] of open.entries()) {\n const status = statuses[index];\n if (status && isBaselineDrifted(status)) {\n drifted.set(snapshot.id, status);\n }\n }\n return drifted;\n}\n\n/** 409 telling the client the diff is out of date; recompute it or force the approval. */\nexport function baselineChanged(snapshotId: string, status: BaselineStatus): HTTPException {\n const body = {\n code: \"baseline_changed\",\n message:\n \"The baseline changed after this diff was computed. Re-diff the build, or approve with force.\",\n snapshotId,\n baselineStatus: status,\n };\n return new HTTPException(409, {\n res: new Response(JSON.stringify(body), {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n }),\n });\n}\n\n/** Throw {@link baselineChanged} when an open snapshot's baseline drifted and `force` is off. */\nexport async function assertBaselineCurrent(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n force: boolean,\n): Promise<void> {\n if (!isOpenSnapshot(snapshot)) {\n return;\n }\n const status = await snapshotBaselineStatus(project, build, snapshot);\n if (!isBaselineDrifted(status)) {\n return;\n }\n if (force) {\n getStore().logger.warn(\n { snapshotId: snapshot.id, baselineStatus: status, userId: getStore().user?.id ?? null },\n \"approved snapshot despite changed baseline (forced)\",\n );\n return;\n }\n throw baselineChanged(snapshot.id, status);\n}\n\n/** True when `error` is the 409 raised by {@link baselineChanged}. */\nexport function isBaselineChanged(error: unknown): boolean {\n return error instanceof HTTPException && error.status === 409;\n}\n\n/** Per-snapshot baseline facts the review pages need. */\nexport interface BaselineView {\n /** Snapshot ids that have a baseline to compare against. */\n hasBaseline: Record<string, boolean>;\n /** Open snapshots whose recorded baseline no longer matches the current one. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** Resolve each snapshot's current baseline once and derive presence plus drift. */\nexport async function loadBaselineView(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\" | \"isDefault\">,\n snapshots: Snapshot[],\n): Promise<BaselineView> {\n const baselines = new BaselineModel(getStore().db);\n const entries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return { snapshot, current };\n }),\n );\n const view: BaselineView = { hasBaseline: {}, drifted: {} };\n for (const { snapshot, current } of entries) {\n view.hasBaseline[snapshot.id] = Boolean(snapshot.comparisonImagePath ?? current);\n const status = baselineStatus(snapshot, current);\n if (\n !build.isDefault &&\n isOpenSnapshot(snapshot) &&\n (status === \"stale\" || status === \"removed\")\n ) {\n view.drifted[snapshot.id] = status;\n }\n }\n return view;\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,eAAe,UAA6C;CAC1E,OAAO,SAAS,WAAW,SAAS,SAAS,WAAW;AAC1D;;AAGA,eAAsB,uBACpB,SACA,OACA,UACyB;CAEzB,MAAM,UAAU,MAAM,IADA,cAAc,SAAS,CAAC,CAAC,EACjB,CAAC,CAAC,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,OAAO,eAAe,UAAU,OAAO;AACzC;;AAGA,eAAsB,qBACpB,SACA,OACA,WACsC;CACtC,MAAM,OAAO,UAAU,QAAQ,aAAa,eAAe,QAAQ,CAAC;CACpE,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,aAAa,MAAM,uBAAuB,SAAS,OAAO,QAAQ,CAAC,CACrF;CACA,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,CAAC,OAAO,aAAa,KAAK,QAAQ,GAAG;EAC9C,MAAM,SAAS,SAAS;EACxB,IAAI,UAAU,kBAAkB,MAAM,GACpC,QAAQ,IAAI,SAAS,IAAI,MAAM;CAEnC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,YAAoB,QAAuC;CAQzF,OAAO,IAAI,cAAc,KAAK,EAC5B,KAAK,IAAI,SAAS,KAAK,UAAU;EAPjC,MAAM;EACN,SACE;EACF;EACA,gBAAgB;CAGoB,CAAC,GAAG;EACtC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC,EACH,CAAC;AACH;;AAGA,eAAsB,sBACpB,SACA,OACA,UACA,OACe;CACf,IAAI,CAAC,eAAe,QAAQ,GAC1B;CAEF,MAAM,SAAS,MAAM,uBAAuB,SAAS,OAAO,QAAQ;CACpE,IAAI,CAAC,kBAAkB,MAAM,GAC3B;CAEF,IAAI,OAAO;EACT,SAAS,CAAC,CAAC,OAAO,KAChB;GAAE,YAAY,SAAS;GAAI,gBAAgB;GAAQ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;EAAK,GACvF,qDACF;EACA;CACF;CACA,MAAM,gBAAgB,SAAS,IAAI,MAAM;AAC3C;;AAGA,SAAgB,kBAAkB,OAAyB;CACzD,OAAO,iBAAiB,iBAAiB,MAAM,WAAW;AAC5D;;AAWA,eAAsB,iBACpB,SACA,OACA,WACuB;CACvB,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,UAAU,MAAM,QAAQ,IAC5B,UAAU,IAAI,OAAO,aAAa;EAShC,OAAO;GAAE;GAAU,SAAA,MARG,UAAU,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;EAC2B;CAC7B,CAAC,CACH;CACA,MAAM,OAAqB;EAAE,aAAa,CAAC;EAAG,SAAS,CAAC;CAAE;CAC1D,KAAK,MAAM,EAAE,UAAU,aAAa,SAAS;EAC3C,KAAK,YAAY,SAAS,MAAM,QAAQ,SAAS,uBAAuB,OAAO;EAC/E,MAAM,SAAS,eAAe,UAAU,OAAO;EAC/C,IACE,CAAC,MAAM,aACP,eAAe,QAAQ,MACtB,WAAW,WAAW,WAAW,YAElC,KAAK,QAAQ,SAAS,MAAM;CAEhC;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { readComparisonImage, validFrozenReference } from "./comparison-image.mjs";
|
|
6
|
+
import { BaselineModel, BuildModel, SnapshotModel, baselineStatus, resolveBaselineOrigin } from "@storyshelf/core/models";
|
|
7
|
+
// src/routers/baseline-reference.ts
|
|
8
|
+
async function loadBaselineReference(project, build, snapshot) {
|
|
9
|
+
if (snapshot.comparisonImagePath || snapshot.comparisonMetadata) return await frozenReference(project, build, snapshot);
|
|
10
|
+
return await legacyReference(project, build, snapshot);
|
|
11
|
+
}
|
|
12
|
+
async function legacyReference(project, build, snapshot) {
|
|
13
|
+
if (snapshot.baselineVersion === "none" && snapshot.baselineId === null) return { kind: "none" };
|
|
14
|
+
if (!snapshot.baselineId || !snapshot.baselineVersion) return { kind: "unavailable" };
|
|
15
|
+
const db = getStore().db;
|
|
16
|
+
const baseline = await new BaselineModel(db).resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
|
|
17
|
+
if (!baseline || baselineStatus(snapshot, baseline) !== "current") return { kind: "unavailable" };
|
|
18
|
+
const snapshots = new SnapshotModel(db);
|
|
19
|
+
const builds = new BuildModel(db);
|
|
20
|
+
const reference = await resolveBaselineOrigin(baseline, {
|
|
21
|
+
baseline: async (id) => await db.get(db.tables.baselines, id),
|
|
22
|
+
snapshot: async (id) => await snapshots.get(id),
|
|
23
|
+
build: async (id) => await builds.get(id)
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
kind: "known",
|
|
27
|
+
branch: baseline.branch,
|
|
28
|
+
defaultBranch: baseline.branch === project.gitDefaultBranch,
|
|
29
|
+
updatedAt: baseline.updatedAt,
|
|
30
|
+
imageAvailable: await imageExists(baseline.screenshotPath),
|
|
31
|
+
build: reference
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function frozenReference(project, build, snapshot) {
|
|
35
|
+
const frozen = validFrozenReference(snapshot, project.id, build.id);
|
|
36
|
+
if (!frozen) return { kind: "unavailable" };
|
|
37
|
+
const { reference: metadata, path } = frozen;
|
|
38
|
+
const reference = metadata.sourceBuildId ? await new BuildModel(getStore().db).get(metadata.sourceBuildId) : null;
|
|
39
|
+
return {
|
|
40
|
+
kind: "known",
|
|
41
|
+
branch: metadata.branch,
|
|
42
|
+
defaultBranch: metadata.defaultBranch,
|
|
43
|
+
updatedAt: metadata.baselineVersion,
|
|
44
|
+
imageAvailable: Boolean(await readComparisonImage(path, metadata.imageSha256)),
|
|
45
|
+
build: reference?.projectId === project.id ? reference : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** A separate, read-only view of the current default-branch reference. */
|
|
49
|
+
async function loadDefaultComparison(project, build, snapshot) {
|
|
50
|
+
if (build.gitBranch === project.gitDefaultBranch) return null;
|
|
51
|
+
const { db } = getStore();
|
|
52
|
+
const baseline = await new BaselineModel(db).getFor(project.id, snapshot.storyId, snapshot.viewportName, project.gitDefaultBranch, snapshot.browser);
|
|
53
|
+
if (!baseline || baseline.id === snapshot.baselineId && baseline.updatedAt === snapshot.baselineVersion) return null;
|
|
54
|
+
const environment = !baseline.infraHash || !snapshot.infraHash ? "unknown" : baseline.infraHash === snapshot.infraHash ? "matching" : "different";
|
|
55
|
+
return {
|
|
56
|
+
id: baseline.id,
|
|
57
|
+
version: baseline.updatedAt,
|
|
58
|
+
branch: baseline.branch,
|
|
59
|
+
available: await imageExists(baseline.screenshotPath),
|
|
60
|
+
environment
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function imageExists(path) {
|
|
64
|
+
try {
|
|
65
|
+
return await getStore().storage.exists(path);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export { loadBaselineReference, loadDefaultComparison };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=baseline-reference.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline-reference.mjs","names":[],"sources":["../../src/routers/baseline-reference.ts"],"sourcesContent":["import {\n BaselineModel,\n BuildModel,\n SnapshotModel,\n baselineStatus,\n resolveBaselineOrigin,\n} from \"@storyshelf/core/models\";\nimport type { Baseline, Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { getStore } from \"../store.ts\";\nimport { readComparisonImage, validFrozenReference } from \"./comparison-image.ts\";\n\n/** Reference information is only reliable while the recorded baseline version still exists. */\nexport type BaselineReference =\n | { kind: \"none\" }\n | { kind: \"unavailable\" }\n | {\n kind: \"known\";\n branch: string;\n defaultBranch: boolean;\n updatedAt: string;\n build: Build | null;\n imageAvailable: boolean;\n };\n\nexport async function loadBaselineReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n if (snapshot.comparisonImagePath || snapshot.comparisonMetadata) {\n return await frozenReference(project, build, snapshot);\n }\n return await legacyReference(project, build, snapshot);\n}\n\nasync function legacyReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n if (snapshot.baselineVersion === \"none\" && snapshot.baselineId === null) return { kind: \"none\" };\n if (!snapshot.baselineId || !snapshot.baselineVersion) return { kind: \"unavailable\" };\n const db = getStore().db;\n const baseline = await new BaselineModel(db).resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (!baseline || baselineStatus(snapshot, baseline) !== \"current\") return { kind: \"unavailable\" };\n const snapshots = new SnapshotModel(db);\n const builds = new BuildModel(db);\n const reference = await resolveBaselineOrigin(baseline, {\n baseline: async (id) => (await db.get(db.tables.baselines, id)) as unknown as Baseline | null,\n snapshot: async (id) => await snapshots.get(id),\n build: async (id) => await builds.get(id),\n });\n return {\n kind: \"known\",\n branch: baseline.branch,\n defaultBranch: baseline.branch === project.gitDefaultBranch,\n updatedAt: baseline.updatedAt,\n imageAvailable: await imageExists(baseline.screenshotPath),\n build: reference,\n };\n}\n\nasync function frozenReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n const frozen = validFrozenReference(snapshot, project.id, build.id);\n if (!frozen) return { kind: \"unavailable\" };\n const { reference: metadata, path } = frozen;\n const reference = metadata.sourceBuildId\n ? await new BuildModel(getStore().db).get(metadata.sourceBuildId)\n : null;\n return {\n kind: \"known\",\n branch: metadata.branch,\n defaultBranch: metadata.defaultBranch,\n updatedAt: metadata.baselineVersion,\n imageAvailable: Boolean(await readComparisonImage(path, metadata.imageSha256)),\n build: reference?.projectId === project.id ? reference : null,\n };\n}\n\nexport interface DefaultComparison {\n id: string;\n version: string;\n branch: string;\n available: boolean;\n environment: \"unknown\" | \"matching\" | \"different\";\n}\n\n/** A separate, read-only view of the current default-branch reference. */\nexport async function loadDefaultComparison(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<DefaultComparison | null> {\n if (build.gitBranch === project.gitDefaultBranch) return null;\n const { db } = getStore();\n const baseline = await new BaselineModel(db).getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (\n !baseline ||\n (baseline.id === snapshot.baselineId && baseline.updatedAt === snapshot.baselineVersion)\n )\n return null;\n const environment =\n !baseline.infraHash || !snapshot.infraHash\n ? \"unknown\"\n : baseline.infraHash === snapshot.infraHash\n ? \"matching\"\n : \"different\";\n return {\n id: baseline.id,\n version: baseline.updatedAt,\n branch: baseline.branch,\n available: await imageExists(baseline.screenshotPath),\n environment,\n };\n}\n\nasync function imageExists(path: string): Promise<boolean> {\n try {\n return await getStore().storage.exists(path);\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;AAwBA,eAAsB,sBACpB,SACA,OACA,UAC4B;CAC5B,IAAI,SAAS,uBAAuB,SAAS,oBAC3C,OAAO,MAAM,gBAAgB,SAAS,OAAO,QAAQ;CAEvD,OAAO,MAAM,gBAAgB,SAAS,OAAO,QAAQ;AACvD;AAEA,eAAe,gBACb,SACA,OACA,UAC4B;CAC5B,IAAI,SAAS,oBAAoB,UAAU,SAAS,eAAe,MAAM,OAAO,EAAE,MAAM,OAAO;CAC/F,IAAI,CAAC,SAAS,cAAc,CAAC,SAAS,iBAAiB,OAAO,EAAE,MAAM,cAAc;CACpF,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,WAAW,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,QAC3C,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,IAAI,CAAC,YAAY,eAAe,UAAU,QAAQ,MAAM,WAAW,OAAO,EAAE,MAAM,cAAc;CAChG,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,YAAY,MAAM,sBAAsB,UAAU;EACtD,UAAU,OAAO,OAAQ,MAAM,GAAG,IAAI,GAAG,OAAO,WAAW,EAAE;EAC7D,UAAU,OAAO,OAAO,MAAM,UAAU,IAAI,EAAE;EAC9C,OAAO,OAAO,OAAO,MAAM,OAAO,IAAI,EAAE;CAC1C,CAAC;CACD,OAAO;EACL,MAAM;EACN,QAAQ,SAAS;EACjB,eAAe,SAAS,WAAW,QAAQ;EAC3C,WAAW,SAAS;EACpB,gBAAgB,MAAM,YAAY,SAAS,cAAc;EACzD,OAAO;CACT;AACF;AAEA,eAAe,gBACb,SACA,OACA,UAC4B;CAC5B,MAAM,SAAS,qBAAqB,UAAU,QAAQ,IAAI,MAAM,EAAE;CAClE,IAAI,CAAC,QAAQ,OAAO,EAAE,MAAM,cAAc;CAC1C,MAAM,EAAE,WAAW,UAAU,SAAS;CACtC,MAAM,YAAY,SAAS,gBACvB,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,aAAa,IAC9D;CACJ,OAAO;EACL,MAAM;EACN,QAAQ,SAAS;EACjB,eAAe,SAAS;EACxB,WAAW,SAAS;EACpB,gBAAgB,QAAQ,MAAM,oBAAoB,MAAM,SAAS,WAAW,CAAC;EAC7E,OAAO,WAAW,cAAc,QAAQ,KAAK,YAAY;CAC3D;AACF;;AAWA,eAAsB,sBACpB,SACA,OACA,UACmC;CACnC,IAAI,MAAM,cAAc,QAAQ,kBAAkB,OAAO;CACzD,MAAM,EAAE,OAAO,SAAS;CACxB,MAAM,WAAW,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,OAC3C,QAAQ,IACR,SAAS,SACT,SAAS,cACT,QAAQ,kBACR,SAAS,OACX;CACA,IACE,CAAC,YACA,SAAS,OAAO,SAAS,cAAc,SAAS,cAAc,SAAS,iBAExE,OAAO;CACT,MAAM,cACJ,CAAC,SAAS,aAAa,CAAC,SAAS,YAC7B,YACA,SAAS,cAAc,SAAS,YAC9B,aACA;CACR,OAAO;EACL,IAAI,SAAS;EACb,SAAS,SAAS;EAClB,QAAQ,SAAS;EACjB,WAAW,MAAM,YAAY,SAAS,cAAc;EACpD;CACF;AACF;AAEA,eAAe,YAAY,MAAgC;CACzD,IAAI;EACF,OAAO,MAAM,SAAS,CAAC,CAAC,QAAQ,OAAO,IAAI;CAC7C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
5
5
|
import { BASELINE_ARCHIVE_LIMITS } from "@storyshelf/core/models";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
|
-
|
|
7
|
+
// src/routers/baselines.handlers.ts
|
|
8
8
|
/** HTTP helpers for the baseline export/import router. */
|
|
9
9
|
/** Ceiling for a buffered import archive, whatever `maxUploadBytes` allows. */
|
|
10
10
|
const MAX_BASELINE_IMPORT_BYTES = BASELINE_ARCHIVE_LIMITS.maxArchiveBytes;
|
|
@@ -58,7 +58,6 @@ function toWebStream(chunks) {
|
|
|
58
58
|
function archiveFileName(slug, branch) {
|
|
59
59
|
return `${(branch ? `${slug}-baselines-${branch}` : `${slug}-baselines`).replaceAll(/[^\w.-]+/gu, "_")}.zip`;
|
|
60
60
|
}
|
|
61
|
-
//#endregion
|
|
62
61
|
export { MAX_BASELINE_IMPORT_BYTES, archiveFileName, importByteLimit, readCappedBody, toWebStream };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=baselines.handlers.mjs.map
|
|
@@ -8,7 +8,7 @@ import { archiveFileName, importByteLimit, readCappedBody, toWebStream } from ".
|
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { BaselineArchiveError, BaselineTransferModel } from "@storyshelf/core/models";
|
|
10
10
|
import { HTTPException } from "hono/http-exception";
|
|
11
|
-
|
|
11
|
+
// src/routers/baselines.ts
|
|
12
12
|
/**
|
|
13
13
|
* Baseline portability endpoints: export a project's baselines as a zip
|
|
14
14
|
* (`manifest.json` + PNGs, format v1) and import (upsert) the same format.
|
|
@@ -119,7 +119,6 @@ function registerBaselines(app) {
|
|
|
119
119
|
return c.json(result, 200);
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
//#endregion
|
|
123
122
|
export { registerBaselines };
|
|
124
123
|
|
|
125
124
|
//# sourceMappingURL=baselines.mjs.map
|
|
@@ -11,7 +11,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
|
|
11
11
|
import { BuildModel, CaptureAttemptModel } from "@storyshelf/core/models";
|
|
12
12
|
import { HTTPException } from "hono/http-exception";
|
|
13
13
|
import { CANCELLABLE_BUILD_STATUSES } from "@storyshelf/core/types";
|
|
14
|
-
|
|
14
|
+
// src/routers/build-cancel.ts
|
|
15
15
|
const cancelBuildRoute = createRoute({
|
|
16
16
|
method: "post",
|
|
17
17
|
tags: ["Builds"],
|
|
@@ -62,7 +62,6 @@ async function cancelBuild(buildId) {
|
|
|
62
62
|
await announceBuildStatus(buildId, "cancelled");
|
|
63
63
|
return updated;
|
|
64
64
|
}
|
|
65
|
-
//#endregion
|
|
66
65
|
export { registerBuildCancel };
|
|
67
66
|
|
|
68
67
|
//# sourceMappingURL=build-cancel.mjs.map
|
|
@@ -7,7 +7,7 @@ import { notFound, unauthorized } from "./schemas.mjs";
|
|
|
7
7
|
import { VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { changesetOf } from "@storyshelf/core/models";
|
|
10
|
-
|
|
10
|
+
// src/routers/build-changeset.ts
|
|
11
11
|
const component = z.object({
|
|
12
12
|
importPath: z.string(),
|
|
13
13
|
title: z.string(),
|
|
@@ -64,7 +64,6 @@ function registerBuildChangeset(app) {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
//#endregion
|
|
68
67
|
export { registerBuildChangeset };
|
|
69
68
|
|
|
70
69
|
//# sourceMappingURL=build-changeset.mjs.map
|
|
@@ -8,7 +8,7 @@ import { announceBuildStatus } from "./builds.handlers.mjs";
|
|
|
8
8
|
import { publishingOf } from "@storyshelf/core/capture";
|
|
9
9
|
import { BuildModel, BuildReviewModel, deriveReviewStatus, latestVerdicts } from "@storyshelf/core/models";
|
|
10
10
|
import { HTTPException } from "hono/http-exception";
|
|
11
|
-
|
|
11
|
+
// src/routers/build-reviews.handlers.ts
|
|
12
12
|
/**
|
|
13
13
|
* Build-level verdicts (ADR 0013, decision 3): giving one and reading where a build stands. The
|
|
14
14
|
* routes are classed `review`, so `requireReview` answers 404 for a project with review off.
|
|
@@ -80,7 +80,6 @@ async function decideBuild(project, build) {
|
|
|
80
80
|
function approvalsRequired(project) {
|
|
81
81
|
return Math.max(1, project.reviewApprovals ?? 1);
|
|
82
82
|
}
|
|
83
|
-
//#endregion
|
|
84
83
|
export { giveVerdict, reviewStateOf };
|
|
85
84
|
|
|
86
85
|
//# sourceMappingURL=build-reviews.handlers.mjs.map
|
|
@@ -8,7 +8,7 @@ import { buildReviewCreateSchema, buildReviewSchema, buildReviewStateSchema, for
|
|
|
8
8
|
import { APPROVER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
9
9
|
import { giveVerdict, reviewStateOf } from "./build-reviews.handlers.mjs";
|
|
10
10
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
11
|
-
|
|
11
|
+
// src/routers/build-reviews.ts
|
|
12
12
|
const params = z.object({
|
|
13
13
|
slug: z.string(),
|
|
14
14
|
buildId: z.string()
|
|
@@ -68,7 +68,6 @@ function registerBuildReviews(app) {
|
|
|
68
68
|
return c.json(review, 201);
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
-
//#endregion
|
|
72
71
|
export { registerBuildReviews };
|
|
73
72
|
|
|
74
73
|
//# sourceMappingURL=build-reviews.mjs.map
|
|
@@ -6,14 +6,15 @@ import { getStore } from "../store.mjs";
|
|
|
6
6
|
import { notifyProject } from "../notify.mjs";
|
|
7
7
|
import { notFound } from "./helpers.mjs";
|
|
8
8
|
import { requestBuildState } from "../git-status.mjs";
|
|
9
|
-
import { assertBaselineCurrent } from "./baseline-guard.mjs";
|
|
9
|
+
import { assertBaselineCurrent, baselineChanged } from "./baseline-guard.mjs";
|
|
10
10
|
import { z } from "@hono/zod-openapi";
|
|
11
|
-
import { BaselineModel, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
|
|
11
|
+
import { BaselineApprovalConflict, BaselineModel, BaselineWriteConflict, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
|
|
12
12
|
import { HTTPException } from "hono/http-exception";
|
|
13
13
|
import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
|
|
14
14
|
import { BUILD_STATUSES } from "@storyshelf/core/types";
|
|
15
|
+
import { ReferenceGenerationConflict } from "@storyshelf/core/adapter/database";
|
|
15
16
|
import { Readable, Transform } from "node:stream";
|
|
16
|
-
|
|
17
|
+
// src/routers/builds.handlers.ts
|
|
17
18
|
/**
|
|
18
19
|
* Create a build record with labels and the `build:created` webhook.
|
|
19
20
|
* Shared by the build-creation route.
|
|
@@ -143,13 +144,27 @@ async function approveSnapshot(snapshotId, userId, options = {}) {
|
|
|
143
144
|
if (!project || !build) notFound("Project or build not found");
|
|
144
145
|
if (build.status === "cancelled") throw new HTTPException(409, { message: "The build was cancelled; retry it to review it" });
|
|
145
146
|
await assertBaselineCurrent(project, build, snapshot, options.force ?? false);
|
|
146
|
-
await snapshots.review(snapshotId, "approved", userId);
|
|
147
|
+
if (!db.buildReferenceLifecycle) await snapshots.review(snapshotId, "approved", userId);
|
|
147
148
|
const baselines = new BaselineModel(db, void 0, getStore().storage, credentialKeys(config));
|
|
148
149
|
const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.browser);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
try {
|
|
151
|
+
await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath, void 0, {
|
|
152
|
+
browser: snapshot.browser,
|
|
153
|
+
mode: snapshot.mode,
|
|
154
|
+
approval: db.buildReferenceLifecycle ? {
|
|
155
|
+
snapshot,
|
|
156
|
+
reviewedBy: userId,
|
|
157
|
+
force: options.force ?? false,
|
|
158
|
+
branch: build.gitBranch
|
|
159
|
+
} : void 0
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {
|
|
163
|
+
if (error instanceof BaselineApprovalConflict && error.driftStatus) throw baselineChanged(snapshot.id, error.driftStatus);
|
|
164
|
+
throw new HTTPException(409, { message: error.message });
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
153
168
|
await notifyProject(project, prior ? "baseline:updated" : "baseline:created", {
|
|
154
169
|
storyId: snapshot.storyId,
|
|
155
170
|
viewport: snapshot.viewportName,
|
|
@@ -202,7 +217,6 @@ async function attachLabels(db, projectId, buildId, labels) {
|
|
|
202
217
|
await models.attach(projectId, buildId, key, value);
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
|
-
//#endregion
|
|
206
220
|
export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, announceBuildStatus, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, refreshBuild, snapshotForBuild, storeUploadStream };
|
|
207
221
|
|
|
208
222
|
//# 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 { 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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"}
|
|
1
|
+
{"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport {\n ReferenceGenerationConflict,\n type DatabaseAdapter,\n} from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport {\n BaselineApprovalConflict,\n BaselineModel,\n BaselineWriteConflict,\n} 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, baselineChanged } 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 if (!db.buildReferenceLifecycle) 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 try {\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 {\n browser: snapshot.browser,\n mode: snapshot.mode,\n approval: db.buildReferenceLifecycle\n ? { snapshot, reviewedBy: userId, force: options.force ?? false, branch: build.gitBranch }\n : undefined,\n },\n );\n } catch (error) {\n if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {\n if (error instanceof BaselineApprovalConflict && error.driftStatus)\n throw baselineChanged(snapshot.id, error.driftStatus);\n throw new HTTPException(409, { message: error.message });\n }\n throw error;\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
|
@@ -19,7 +19,7 @@ import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
|
19
19
|
import { storybookZipPath } from "@storyshelf/core/utils";
|
|
20
20
|
import { BuildModel, PackageUsageModel } from "@storyshelf/core/models";
|
|
21
21
|
import { HTTPException } from "hono/http-exception";
|
|
22
|
-
|
|
22
|
+
// src/routers/builds.ts
|
|
23
23
|
/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */
|
|
24
24
|
async function planOrDiscard(zipBytes, projectId, buildId) {
|
|
25
25
|
try {
|
|
@@ -97,8 +97,9 @@ function registerBuilds(app) {
|
|
|
97
97
|
const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
98
98
|
const route = await planOrDiscard(await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes), project.id, build.id);
|
|
99
99
|
const reqId = c.get("requestId");
|
|
100
|
+
const published = await publishUpload(route, build, reqId);
|
|
100
101
|
await getStore().enqueueCapture?.(build.id, reqId);
|
|
101
|
-
return c.json(
|
|
102
|
+
return c.json(published, 202);
|
|
102
103
|
});
|
|
103
104
|
app.openapi(dedupRoute, async (c) => {
|
|
104
105
|
const { slug, buildId } = c.req.valid("param");
|
|
@@ -246,6 +247,10 @@ const uploadZipRoute = createRoute({
|
|
|
246
247
|
content: { "application/json": { schema: buildSchema } },
|
|
247
248
|
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."
|
|
248
249
|
},
|
|
250
|
+
422: {
|
|
251
|
+
content: { "text/plain": { schema: z.string() } },
|
|
252
|
+
description: "The inline zip is invalid, unsafe, or exceeds the extraction limits"
|
|
253
|
+
},
|
|
249
254
|
413: {
|
|
250
255
|
content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
|
|
251
256
|
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)"
|
|
@@ -429,7 +434,6 @@ const deleteBuildRoute = createRoute({
|
|
|
429
434
|
...notFound
|
|
430
435
|
}
|
|
431
436
|
});
|
|
432
|
-
//#endregion
|
|
433
437
|
export { registerBuilds };
|
|
434
438
|
|
|
435
439
|
//# sourceMappingURL=builds.mjs.map
|