@storyshelf/core 0.1.2 → 0.3.1
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 +50 -46
- package/dist/adapter/auth.d.mts +4 -4
- package/dist/adapter/auth.mjs.map +1 -1
- package/dist/{adapters → adapter}/capture-queue.d.mts +6 -4
- package/dist/adapter/capture-queue.mjs +4 -0
- package/dist/adapter/capture-runner.d.mts +7 -7
- package/dist/adapter/database.d.mts +20 -9
- package/dist/adapter/database.mjs +60 -1
- package/dist/adapter/database.mjs.map +1 -0
- package/dist/adapter/git-host/comments.d.mts +41 -0
- package/dist/adapter/git-host/comments.mjs +54 -0
- package/dist/adapter/git-host/comments.mjs.map +1 -0
- package/dist/adapter/git-host/helpers.d.mts +1 -0
- package/dist/adapter/git-host/helpers.mjs +1 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapter/git-host.d.mts +3 -7
- package/dist/adapter/init.d.mts +58 -0
- package/dist/adapter/init.mjs +93 -0
- package/dist/adapter/init.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +83 -0
- package/dist/adapter/metadata.mjs +4 -0
- package/dist/adapter/storage.d.mts +15 -4
- package/dist/adapters/webhook-events.mjs +20 -4
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +7 -1
- package/dist/capture/adapter.mjs +3 -0
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/dispatch.mjs +150 -0
- package/dist/capture/dispatch.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +9 -0
- package/dist/capture/orchestrator.mjs +11 -44
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +4 -1
- package/dist/capture/pipeline.mjs +6 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.d.mts +4 -1
- package/dist/capture/queue.mjs +3 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +76 -0
- package/dist/capture/skip-checks.mjs.map +1 -0
- package/dist/capture/statics.mjs +124 -0
- package/dist/capture/statics.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs +1 -0
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +1 -0
- package/dist/capture/storybook.mjs +1 -0
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture.d.mts +8 -0
- package/dist/capture.mjs +9 -0
- package/dist/config.d.mts +37 -22
- package/dist/config.mjs +28 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +2 -1
- package/dist/ddl.mjs +2 -1
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +8 -0
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/diff/options.d.mts +3 -1
- package/dist/diff.d.mts +5 -0
- package/dist/diff.mjs +6 -0
- package/dist/index.d.mts +34 -24
- package/dist/index.mjs +211 -252
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +1 -0
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +1 -0
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +28 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +29 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/models/baseline.mjs +9 -5
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.mjs +5 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/comment.mjs +4 -1
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/label.mjs +3 -1
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.mjs +2 -1
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/project.mjs +2 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.mjs +2 -1
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.mjs +2 -1
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.mjs +2 -1
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/webhook.mjs +12 -3
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +10 -4
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +113 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +163 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +66 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +197 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +66 -413
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs +3 -1
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +4 -3
- package/dist/pages/label-detail.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -0
- package/dist/pages/login.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +6 -3
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -0
- package/dist/pages/project-create.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +1 -0
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs +12 -6
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/root.mjs +7 -3
- package/dist/pages/root.mjs.map +1 -1
- package/dist/pages/settings-general.mjs +1 -0
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +2 -1
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs +2 -1
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-status.mjs +3 -1
- package/dist/pages/settings-status.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +4 -3
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs +1 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +38 -1
- package/dist/pages/storybook.mjs.map +1 -1
- package/dist/{utils/paths.d.mts → paths.d.mts} +5 -0
- package/dist/{utils/paths.mjs → paths.mjs} +5 -0
- package/dist/paths.mjs.map +1 -0
- package/dist/retention/purge.mjs +2 -1
- package/dist/retention/purge.mjs.map +1 -1
- package/dist/routers/admin.mjs +2 -1
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -0
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -0
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/builds.handlers.mjs +114 -19
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +55 -34
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/comments.mjs +1 -0
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/health.mjs +137 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +8 -2
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +2 -0
- package/dist/routers/htmx.mjs.map +1 -1
- package/dist/routers/labels.mjs +2 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/media.mjs +3 -2
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -0
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/projects.mjs +2 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/schemas.mjs +25 -0
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings-general.mjs +95 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +49 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +30 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +73 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +63 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +93 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +14 -246
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/snapshots.mjs +2 -1
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/status-configs.mjs +2 -1
- package/dist/routers/status-configs.mjs.map +1 -1
- package/dist/routers/storybook.mjs +12 -4
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -1
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +5 -4
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +7 -6
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +18 -0
- package/dist/schema/baseline.mjs +22 -0
- package/dist/schema/baseline.mjs.map +1 -0
- package/dist/schema/build.d.mts +26 -0
- package/dist/schema/build.mjs +29 -0
- package/dist/schema/build.mjs.map +1 -0
- package/dist/schema/comment.d.mts +19 -0
- package/dist/schema/comment.mjs +26 -0
- package/dist/schema/comment.mjs.map +1 -0
- package/dist/schema/label.d.mts +25 -0
- package/dist/schema/label.mjs +30 -0
- package/dist/schema/label.mjs.map +1 -0
- package/dist/schema/member.d.mts +15 -0
- package/dist/schema/member.mjs +19 -0
- package/dist/schema/member.mjs.map +1 -0
- package/dist/schema/project.d.mts +34 -0
- package/dist/schema/project.mjs +25 -0
- package/dist/schema/project.mjs.map +1 -0
- package/dist/schema/snapshot.d.mts +30 -0
- package/dist/schema/snapshot.mjs +34 -0
- package/dist/schema/snapshot.mjs.map +1 -0
- package/dist/schema/status-config.d.mts +16 -0
- package/dist/schema/status-config.mjs +20 -0
- package/dist/schema/status-config.mjs.map +1 -0
- package/dist/schema/token.d.mts +15 -0
- package/dist/schema/token.mjs +19 -0
- package/dist/schema/token.mjs.map +1 -0
- package/dist/schema/user.d.mts +17 -0
- package/dist/schema/user.mjs +19 -0
- package/dist/schema/user.mjs.map +1 -0
- package/dist/schema/webhook.d.mts +16 -0
- package/dist/schema/webhook.mjs +20 -0
- package/dist/schema/webhook.mjs.map +1 -0
- package/dist/schema.d.mts +52 -4433
- package/dist/schema.mjs +37 -139
- package/dist/schema.mjs.map +1 -1
- package/dist/store.mjs +2 -0
- package/dist/store.mjs.map +1 -1
- package/dist/types.d.mts +13 -2
- package/dist/types.mjs +8 -0
- package/dist/types.mjs.map +1 -1
- package/dist/ui/document.mjs +1 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/feedback.mjs +24 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/{components.mjs → forms.mjs} +6 -16
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/styles.mjs +1 -0
- package/dist/ui/styles.mjs.map +1 -1
- package/dist/ui/theme.mjs +2 -0
- package/dist/ui/theme.mjs.map +1 -1
- package/dist/urls.d.mts +8 -0
- package/dist/urls.mjs +7 -0
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/hash.mjs +3 -0
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/ulid.mjs +2 -0
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +47 -45
- package/dist/adapters/metadata.d.mts +0 -24
- package/dist/models/baseline.d.mts +0 -20
- package/dist/models/build.d.mts +0 -50
- package/dist/models/comment.d.mts +0 -20
- package/dist/models/label.d.mts +0 -49
- package/dist/models/member.d.mts +0 -27
- package/dist/models/project.d.mts +0 -42
- package/dist/models/snapshot.d.mts +0 -46
- package/dist/models/token.d.mts +0 -33
- package/dist/models/webhook.d.mts +0 -37
- package/dist/retention/purge.d.mts +0 -28
- package/dist/ui/components.mjs.map +0 -1
- package/dist/ui/document.d.mts +0 -8
- package/dist/utils/paths.mjs.map +0 -1
- package/dist/utils/ulid.d.mts +0 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { CaptureRunner } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from \"./adapter.ts\";\nimport type { Viewport } from \"./adapter.ts\";\nimport { persistCapture } from \"./pipeline.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"./statics.ts\";\nimport { StorybookAdapter } from \"./storybook.ts\";\n\n/** Inputs for running a capture job against a Storybook build. */\nexport interface CaptureJobOptions {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n runner: CaptureRunner;\n scratchDir: string;\n viewports?: Viewport[];\n logger?: Logger;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: string | undefined;\n}\n/**\n * Run the full capture for a build: extract, render, persist, and finalize.\n *\n * @param input - Build id plus the originating request id.\n * @param options - Adapters, scratch dir, viewports, and logger.\n */\nexport async function executeCaptureJob(\n input: { buildId: string; reqId?: string },\n options: CaptureJobOptions,\n): Promise<void> {\n const builds = new BuildModel(options.db);\n const { build, project } = await loadTarget(options, input.buildId);\n const logger = options.logger?.child({ buildId: input.buildId, reqId: input.reqId });\n await builds.setStatus(build.id, \"capturing\");\n\n const startTime = performance.now();\n let extractedDir: string | undefined;\n try {\n const extractStart = performance.now();\n extractedDir = await extractStorybookToScratch(\n options.storage,\n options.scratchDir,\n project.id,\n build.id,\n );\n const extractDuration = performance.now() - extractStart;\n logger?.info({ durationMs: Math.round(extractDuration) }, \"storybook extracted\");\n\n // Persist the extracted statics to storage so the published Storybook\n // (`storybookDir`) can be served after the scratch dir is cleaned up.\n const staticsStart = performance.now();\n await persistStorybookStatics(options.storage, extractedDir, project.id, build.id);\n logger?.info(\n { durationMs: Math.round(performance.now() - staticsStart) },\n \"storybook statics persisted\",\n );\n\n const adapter = new StorybookAdapter();\n const discovered = await adapter.discover(extractedDir);\n const stories = discovered.filter((s) => !isDisabledStory(s));\n const viewports = options.viewports ?? DEFAULT_VIEWPORTS;\n\n const renderStart = performance.now();\n const result = await options.runner.render({\n buildId: build.id,\n storybookDir: extractedDir,\n stories,\n viewports,\n logger,\n executePlay: project.executePlay ?? false,\n playTimeoutMs: project.playTimeoutMs ?? 10_000,\n });\n const renderDuration = performance.now() - renderStart;\n logger?.info(\n { durationMs: Math.round(renderDuration), storyCount: stories.length },\n \"stories rendered\",\n );\n\n const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));\n const blockingFailed = new Set<string>();\n const flakyFailed = new Set<string>();\n for (const f of result.failures) {\n if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);\n else blockingFailed.add(f.storyId);\n }\n\n const persistStart = performance.now();\n await persistCapture(\n {\n db: options.db,\n storage: options.storage,\n project,\n build,\n viewports,\n captures: result.captures,\n logger,\n secret: options.secret,\n },\n blockingFailed,\n flakyFailed,\n );\n const persistDuration = performance.now() - persistStart;\n logger?.info({ durationMs: Math.round(persistDuration) }, \"capture persisted\");\n\n const totalDuration = performance.now() - startTime;\n logger?.info({ durationMs: Math.round(totalDuration) }, \"capture completed\");\n } catch (error) {\n const totalDuration = performance.now() - startTime;\n logger?.error({ durationMs: Math.round(totalDuration), err: error }, \"capture failed\");\n await builds.setStatus(build.id, \"failed\").catch((markError: unknown) => {\n logger?.error({ err: markError }, \"failed to mark build failed after capture error\");\n });\n throw error;\n }\n}\n\nasync function loadTarget(\n options: CaptureJobOptions,\n buildId: string,\n): Promise<{ build: Build; project: Project }> {\n const build = await new BuildModel(options.db).get(buildId);\n if (!build) {\n throw new Error(`Build not found: ${buildId}`);\n }\n const project = await new ProjectModel(options.db).get(build.projectId);\n if (!project) {\n throw new Error(`Project not found: ${build.projectId}`);\n }\n return { build, project };\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { Viewport } from "./adapter.mjs";
|
|
|
4
4
|
import { RenderedSnapshot } from "../adapter/capture-runner.mjs";
|
|
5
5
|
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
6
6
|
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
7
|
-
import { Build
|
|
7
|
+
import { Build } from "../schema/build.mjs";
|
|
8
|
+
import { Project } from "../schema/project.mjs";
|
|
8
9
|
import { Logger } from "pino";
|
|
9
10
|
//#region src/capture/pipeline.d.ts
|
|
10
11
|
/** Persistence inputs for a completed capture run. */
|
|
@@ -23,6 +24,8 @@ interface CaptureContext {
|
|
|
23
24
|
captures: RenderedSnapshot[];
|
|
24
25
|
/** Optional logger for capture diagnostics. */
|
|
25
26
|
logger?: Logger;
|
|
27
|
+
/** Server secret for decrypting webhook secrets at send time. */
|
|
28
|
+
secret?: string | undefined;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* Persist a completed capture run: write screenshots, diff against the branch
|
|
@@ -2,11 +2,11 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../models/build.mjs";
|
|
5
|
-
import { diffPath, screenshotPath } from "../utils/paths.mjs";
|
|
6
|
-
import { BaselineModel } from "../models/baseline.mjs";
|
|
7
|
-
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
8
5
|
import { diffImages } from "../diff/engine.mjs";
|
|
9
6
|
import { DEFAULT_DIFF_OPTIONS } from "../diff/options.mjs";
|
|
7
|
+
import { diffPath, screenshotPath } from "../paths.mjs";
|
|
8
|
+
import { BaselineModel } from "../models/baseline.mjs";
|
|
9
|
+
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
10
10
|
//#region src/capture/pipeline.ts
|
|
11
11
|
/**
|
|
12
12
|
* Persist a completed capture run: write screenshots, diff against the branch
|
|
@@ -65,7 +65,7 @@ async function persistSnapshot(ctx, capture) {
|
|
|
65
65
|
await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);
|
|
66
66
|
}
|
|
67
67
|
async function resolveBaseline(ctx, storyId, viewport) {
|
|
68
|
-
return await new BaselineModel(ctx.db, ctx.storage).resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);
|
|
68
|
+
return await new BaselineModel(ctx.db, ctx.storage, ctx.secret).resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);
|
|
69
69
|
}
|
|
70
70
|
async function createWithoutBaseline(ctx, capture, viewport, screenshot) {
|
|
71
71
|
const snapshots = new SnapshotModel(ctx.db);
|
|
@@ -81,7 +81,7 @@ async function createWithoutBaseline(ctx, capture, viewport, screenshot) {
|
|
|
81
81
|
screenshotPath: screenshot
|
|
82
82
|
});
|
|
83
83
|
await snapshots.setStatus(snapshot.id, status);
|
|
84
|
-
if (ctx.build.isDefault) await new BaselineModel(ctx.db, ctx.storage).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);
|
|
84
|
+
if (ctx.build.isDefault) await new BaselineModel(ctx.db, ctx.storage, ctx.secret).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);
|
|
85
85
|
}
|
|
86
86
|
async function createWithBaseline(ctx, capture, viewport, screenshot, baselinePath) {
|
|
87
87
|
const current = await ctx.storage.read(screenshot);
|
|
@@ -123,7 +123,7 @@ async function finalize(ctx, storyIds, failedStoryIds, flakyFailedStoryIds = /*
|
|
|
123
123
|
else if (hasCaptures && build.changedCount === 0) status = "approved";
|
|
124
124
|
if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, "flaky stories failed (non-blocking)");
|
|
125
125
|
await builds.setStatus(ctx.build.id, status);
|
|
126
|
-
if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.storage).removeOrphans(ctx.project.id, new Set(storyIds));
|
|
126
|
+
if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.storage, ctx.secret).removeOrphans(ctx.project.id, new Set(storyIds));
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
129
|
export { persistCapture };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../../src/capture/pipeline.ts"],"sourcesContent":["import type { Logger } from \"pino\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { RenderedSnapshot } from \"../adapters/capture-runner.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { diffImages } from \"../diff/engine.ts\";\nimport { DEFAULT_DIFF_OPTIONS } from \"../diff/options.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline, Build, Project } from \"../schema.ts\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { diffPath, screenshotPath } from \"../utils/paths.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\n/** Persistence inputs for a completed capture run. */\nexport interface CaptureContext {\n /** Database adapter. */\n db: DatabaseAdapter;\n /** Storage adapter. */\n storage: StorageAdapter;\n /** The project being captured. */\n project: Project;\n /** The build being captured. */\n build: Build;\n /** Viewports at which stories were captured. */\n viewports: Viewport[];\n /** Screenshot buffers produced by the capture renderer. */\n captures: RenderedSnapshot[];\n /** Optional logger for capture diagnostics. */\n logger?: Logger;\n}\n\n/**\n * Persist a completed capture run: write screenshots, diff against the branch\n * baseline, create snapshots, and finalize the build.\n *\n * Rendering is performed by a pure `CaptureRunner`; this function owns only\n * storage writes and record keeping, so it can run against any renderer.\n *\n * @param ctx - Capture context.\n * @param renderFailedStoryIds - Story ids that the renderer could not capture\n * (blocking failures). They force the build to `failed`.\n * @param flakyFailedStoryIds - Story ids that failed but are marked flaky\n * (non-blocking). They are logged as warnings and do not block the build.\n */\nexport async function persistCapture(\n ctx: CaptureContext,\n renderFailedStoryIds: ReadonlySet<string> = new Set(),\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n const flakyIds = new Set(flakyFailedStoryIds);\n await Promise.all(\n ctx.captures.map(async (capture) => {\n try {\n await persistSnapshot(ctx, capture);\n } catch (error) {\n // A failure in one story/viewport must not abort the other persists.\n // Flaky stories remain non-blocking even on persist failure.\n if (flakyIds.has(capture.story.id)) {\n flakyIds.add(capture.story.id);\n ctx.logger?.warn(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for flaky story (non-blocking)\",\n );\n } else {\n failedStoryIds.add(capture.story.id);\n }\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n }\n }),\n );\n // Also log flaky render failures as warnings\n for (const id of flakyIds) {\n if (!failedStoryIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"flaky story failed (non-blocking)\");\n }\n }\n await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);\n}\n\nfunction viewportByName(ctx: CaptureContext, name: string): Viewport {\n return ctx.viewports.find((v) => v.name === name) ?? { name, width: 0, height: 0 };\n}\n\nasync function persistSnapshot(ctx: CaptureContext, capture: RenderedSnapshot): Promise<void> {\n const viewport = viewportByName(ctx, capture.viewportName);\n const screenshot = screenshotPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n await ctx.storage.write(screenshot, capture.screenshot);\n\n const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);\n if (!baseline) {\n await createWithoutBaseline(ctx, capture, viewport, screenshot);\n return;\n }\n await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);\n}\n\nasync function resolveBaseline(ctx: CaptureContext, storyId: string, viewport: string): Promise<Baseline | null> {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n return await baselines.resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);\n}\n\nasync function createWithoutBaseline(ctx: CaptureContext, capture: RenderedSnapshot, viewport: Viewport, screenshot: string): Promise<void> {\n const snapshots = new SnapshotModel(ctx.db);\n const status = ctx.build.isDefault ? \"approved\" : \"new\";\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n await snapshots.setStatus(snapshot.id, status);\n\n if (ctx.build.isDefault) {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n await baselines.upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);\n }\n}\n\nasync function createWithBaseline(ctx: CaptureContext, capture: RenderedSnapshot, viewport: Viewport, screenshot: string, baselinePath: string): Promise<void> {\n const current = await ctx.storage.read(screenshot);\n const previous = await ctx.storage.read(baselinePath);\n const options = { ...DEFAULT_DIFF_OPTIONS, pixelThreshold: ctx.project.pixelThreshold, maxDiffRatio: ctx.project.maxDiffRatio };\n const result = diffImages(previous, current, options);\n\n const snapshots = new SnapshotModel(ctx.db);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n\n const status = result.passed ? \"unchanged\" : \"changed\";\n const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n if (!result.passed && result.diffImage) {\n await ctx.storage.write(diff, result.diffImage);\n }\n await snapshots.update(snapshot.id, {\n status,\n diffPath: !result.passed && result.diffImage ? diff : null,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n });\n}\n\nasync function finalize(\n ctx: CaptureContext,\n storyIds: ReadonlySet<string>,\n failedStoryIds: ReadonlySet<string>,\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const builds = new BuildModel(ctx.db);\n const build = await builds.updateCounts(ctx.build.id);\n const hasCaptures = storyIds.size > 0;\n let status: BuildStatus = \"reviewing\";\n if (failedStoryIds.size > 0) {\n status = \"failed\";\n } else if (hasCaptures && build.changedCount === 0) {\n status = \"approved\";\n }\n // Flaky failures do not block: log warning if any flaky stories failed\n if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, \"flaky stories failed (non-blocking)\");\n }\n await builds.setStatus(ctx.build.id, status);\n\n if (ctx.build.isDefault && hasCaptures) {\n const baselines = new BaselineModel(ctx.db, ctx.storage);\n await baselines.removeOrphans(ctx.project.id, new Set(storyIds));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../../src/capture/pipeline.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { RenderedSnapshot } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { diffImages } from \"../diff/engine.ts\";\nimport { DEFAULT_DIFF_OPTIONS } from \"../diff/options.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { diffPath, screenshotPath } from \"../utils/paths.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\n/** Persistence inputs for a completed capture run. */\nexport interface CaptureContext {\n /** Database adapter. */\n db: DatabaseAdapter;\n /** Storage adapter. */\n storage: StorageAdapter;\n /** The project being captured. */\n project: Project;\n /** The build being captured. */\n build: Build;\n /** Viewports at which stories were captured. */\n viewports: Viewport[];\n /** Screenshot buffers produced by the capture renderer. */\n captures: RenderedSnapshot[];\n /** Optional logger for capture diagnostics. */\n logger?: Logger;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: string | undefined;\n}\n\n/**\n * Persist a completed capture run: write screenshots, diff against the branch\n * baseline, create snapshots, and finalize the build.\n *\n * Rendering is performed by a pure `CaptureRunner`; this function owns only\n * storage writes and record keeping, so it can run against any renderer.\n *\n * @param ctx - Capture context.\n * @param renderFailedStoryIds - Story ids that the renderer could not capture\n * (blocking failures). They force the build to `failed`.\n * @param flakyFailedStoryIds - Story ids that failed but are marked flaky\n * (non-blocking). They are logged as warnings and do not block the build.\n */\nexport async function persistCapture(\n ctx: CaptureContext,\n renderFailedStoryIds: ReadonlySet<string> = new Set(),\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n const flakyIds = new Set(flakyFailedStoryIds);\n await Promise.all(\n ctx.captures.map(async (capture) => {\n try {\n await persistSnapshot(ctx, capture);\n } catch (error) {\n // A failure in one story/viewport must not abort the other persists.\n // Flaky stories remain non-blocking even on persist failure.\n if (flakyIds.has(capture.story.id)) {\n flakyIds.add(capture.story.id);\n ctx.logger?.warn(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for flaky story (non-blocking)\",\n );\n } else {\n failedStoryIds.add(capture.story.id);\n }\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n }\n }),\n );\n // Also log flaky render failures as warnings\n for (const id of flakyIds) {\n if (!failedStoryIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"flaky story failed (non-blocking)\");\n }\n }\n await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);\n}\n\nfunction viewportByName(ctx: CaptureContext, name: string): Viewport {\n return ctx.viewports.find((v) => v.name === name) ?? { name, width: 0, height: 0 };\n}\n\nasync function persistSnapshot(ctx: CaptureContext, capture: RenderedSnapshot): Promise<void> {\n const viewport = viewportByName(ctx, capture.viewportName);\n const screenshot = screenshotPath(\n ctx.project.id,\n ctx.build.id,\n capture.story.id,\n capture.viewportName,\n );\n await ctx.storage.write(screenshot, capture.screenshot);\n\n const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);\n if (!baseline) {\n await createWithoutBaseline(ctx, capture, viewport, screenshot);\n return;\n }\n await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);\n}\n\nasync function resolveBaseline(\n ctx: CaptureContext,\n storyId: string,\n viewport: string,\n): Promise<Baseline | null> {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n return await baselines.resolve(\n ctx.project.id,\n storyId,\n viewport,\n ctx.build.gitBranch,\n ctx.project.gitDefaultBranch,\n );\n}\n\nasync function createWithoutBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n): Promise<void> {\n const snapshots = new SnapshotModel(ctx.db);\n const status = ctx.build.isDefault ? \"approved\" : \"new\";\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n await snapshots.setStatus(snapshot.id, status);\n\n if (ctx.build.isDefault) {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n await baselines.upsert(\n ctx.project.id,\n capture.story.id,\n capture.viewportName,\n ctx.build.gitBranch,\n snapshot.id,\n screenshot,\n );\n }\n}\n\nasync function createWithBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n baselinePath: string,\n): Promise<void> {\n const current = await ctx.storage.read(screenshot);\n const previous = await ctx.storage.read(baselinePath);\n const options = {\n ...DEFAULT_DIFF_OPTIONS,\n pixelThreshold: ctx.project.pixelThreshold,\n maxDiffRatio: ctx.project.maxDiffRatio,\n };\n const result = diffImages(previous, current, options);\n\n const snapshots = new SnapshotModel(ctx.db);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n\n const status = result.passed ? \"unchanged\" : \"changed\";\n const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n if (!result.passed && result.diffImage) {\n await ctx.storage.write(diff, result.diffImage);\n }\n await snapshots.update(snapshot.id, {\n status,\n diffPath: !result.passed && result.diffImage ? diff : null,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n });\n}\n\nasync function finalize(\n ctx: CaptureContext,\n storyIds: ReadonlySet<string>,\n failedStoryIds: ReadonlySet<string>,\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const builds = new BuildModel(ctx.db);\n const build = await builds.updateCounts(ctx.build.id);\n const hasCaptures = storyIds.size > 0;\n let status: BuildStatus = \"reviewing\";\n if (failedStoryIds.size > 0) {\n status = \"failed\";\n } else if (hasCaptures && build.changedCount === 0) {\n status = \"approved\";\n }\n // Flaky failures do not block: log warning if any flaky stories failed\n if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn(\n { flakyStoryIds: [...flakyFailedStoryIds] },\n \"flaky stories failed (non-blocking)\",\n );\n }\n await builds.setStatus(ctx.build.id, status);\n\n if (ctx.build.isDefault && hasCaptures) {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n await baselines.removeOrphans(ctx.project.id, new Set(storyIds));\n }\n}\n"],"mappings":"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"}
|
package/dist/capture/queue.d.mts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { CaptureJob, CaptureQueue, QueueEntry } from "../
|
|
3
|
+
import { CaptureJob, CaptureQueue, QueueEntry } from "../adapter/capture-queue.mjs";
|
|
4
4
|
import { Logger } from "pino";
|
|
5
5
|
//#region src/capture/queue.d.ts
|
|
6
|
+
/** Options for the in-process capture queue. */
|
|
6
7
|
interface InMemoryCaptureQueueOptions {
|
|
7
8
|
concurrency: number;
|
|
8
9
|
runJob: (job: CaptureJob) => Promise<void>;
|
|
9
10
|
logger?: Logger;
|
|
10
11
|
}
|
|
12
|
+
/** In-process capture queue with bounded concurrency. */
|
|
11
13
|
declare class InMemoryCaptureQueue implements CaptureQueue {
|
|
12
14
|
private readonly options;
|
|
13
15
|
readonly metadata: {
|
|
@@ -15,6 +17,7 @@ declare class InMemoryCaptureQueue implements CaptureQueue {
|
|
|
15
17
|
readonly version: string;
|
|
16
18
|
readonly description: "In-process capture queue";
|
|
17
19
|
readonly kind: "memory";
|
|
20
|
+
readonly category: "capture-queue";
|
|
18
21
|
};
|
|
19
22
|
private readonly entries;
|
|
20
23
|
private running;
|
package/dist/capture/queue.mjs
CHANGED
|
@@ -2,13 +2,15 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
//#region src/capture/queue.ts
|
|
5
|
+
/** In-process capture queue with bounded concurrency. */
|
|
5
6
|
var InMemoryCaptureQueue = class {
|
|
6
7
|
options;
|
|
7
8
|
metadata = {
|
|
8
9
|
name: "In-Memory Queue",
|
|
9
10
|
version: globalThis.__PKG_VERSION__ ?? "0.0.0",
|
|
10
11
|
description: "In-process capture queue",
|
|
11
|
-
kind: "memory"
|
|
12
|
+
kind: "memory",
|
|
13
|
+
category: "capture-queue"
|
|
12
14
|
};
|
|
13
15
|
entries = /* @__PURE__ */ new Map();
|
|
14
16
|
running = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.mjs","names":[],"sources":["../../src/capture/queue.ts"],"sourcesContent":["import type { Logger } from \"pino\";\
|
|
1
|
+
{"version":3,"file":"queue.mjs","names":[],"sources":["../../src/capture/queue.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { CaptureJob, CaptureQueue, QueueEntry } from \"../adapters/capture-queue.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/** Options for the in-process capture queue. */\nexport interface InMemoryCaptureQueueOptions {\n concurrency: number;\n runJob: (job: CaptureJob) => Promise<void>;\n logger?: Logger;\n}\n/** In-process capture queue with bounded concurrency. */\nexport class InMemoryCaptureQueue implements CaptureQueue {\n readonly metadata = {\n name: \"In-Memory Queue\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"In-process capture queue\",\n kind: \"memory\",\n category: \"capture-queue\",\n } as const;\n\n private readonly entries = new Map<string, QueueEntry>();\n private running = 0;\n private readonly waiting: (() => void)[] = [];\n private readonly inFlight = new Map<string, Promise<void>>();\n\n constructor(private readonly options: InMemoryCaptureQueueOptions) {}\n\n async enqueue(job: CaptureJob): Promise<void> {\n const entry = this.track(job.buildId);\n this.log(job)?.info(\"capture queued\");\n this.inFlight.set(job.buildId, this.process(job, entry));\n await Promise.resolve();\n }\n\n async status(buildId: string): Promise<QueueEntry | null> {\n return await Promise.resolve(this.entries.get(buildId) ?? null);\n }\n\n async active(): Promise<QueueEntry[]> {\n return await Promise.resolve(\n [...this.entries.values()]\n .filter((entry) => entry.status === \"queued\" || entry.status === \"running\")\n .toSorted((a, b) => a.queuedAt.localeCompare(b.queuedAt)),\n );\n }\n\n async recent(limit: number): Promise<QueueEntry[]> {\n return await Promise.resolve(\n [...this.entries.values()]\n .toSorted((a, b) => b.queuedAt.localeCompare(a.queuedAt))\n .slice(0, limit),\n );\n }\n\n private async process(job: CaptureJob, entry: QueueEntry): Promise<void> {\n const log = this.log(job);\n await this.acquire();\n entry.status = \"running\";\n entry.startedAt = new Date().toISOString();\n log?.info(\"capture started\");\n try {\n await this.options.runJob(job);\n } catch (error) {\n entry.status = \"failed\";\n entry.finishedAt = new Date().toISOString();\n entry.error = error instanceof Error ? error.message : \"Capture failed\";\n log?.error({ err: error }, \"capture failed\");\n } finally {\n this.running -= 1;\n this.waiting.shift()?.();\n if (entry.status !== \"failed\") {\n entry.status = \"completed\";\n entry.finishedAt = new Date().toISOString();\n log?.info(\"capture completed\");\n }\n }\n }\n\n private log(job: CaptureJob): Logger | undefined {\n return this.options.logger?.child({ buildId: job.buildId, reqId: job.reqId });\n }\n\n private track(buildId: string): QueueEntry {\n const existing = this.entries.get(buildId);\n if (existing) {\n existing.status = \"queued\";\n existing.queuedAt = new Date().toISOString();\n existing.startedAt = undefined;\n existing.finishedAt = undefined;\n existing.error = undefined;\n return existing;\n }\n const entry: QueueEntry = { buildId, status: \"queued\", queuedAt: new Date().toISOString() };\n this.entries.set(buildId, entry);\n return entry;\n }\n\n private async acquire(): Promise<void> {\n if (this.running < this.options.concurrency) {\n this.running += 1;\n return;\n }\n await new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n await this.acquire();\n }\n}\n"],"mappings":";;;;;AAYA,IAAa,uBAAb,MAA0D;CAc3B;CAb7B,WAAoB;EAClB,MAAM;EACN,SAAU,WAAuD,mBAAmB;EACpF,aAAa;EACb,MAAM;EACN,UAAU;CACZ;CAEA,0BAA2B,IAAI,IAAwB;CACvD,UAAkB;CAClB,UAA2C,CAAC;CAC5C,2BAA4B,IAAI,IAA2B;CAE3D,YAAY,SAAuD;EAAtC,KAAA,UAAA;CAAuC;CAEpE,MAAM,QAAQ,KAAgC;EAC5C,MAAM,QAAQ,KAAK,MAAM,IAAI,OAAO;EACpC,KAAK,IAAI,GAAG,CAAC,EAAE,KAAK,gBAAgB;EACpC,KAAK,SAAS,IAAI,IAAI,SAAS,KAAK,QAAQ,KAAK,KAAK,CAAC;EACvD,MAAM,QAAQ,QAAQ;CACxB;CAEA,MAAM,OAAO,SAA6C;EACxD,OAAO,MAAM,QAAQ,QAAQ,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;CAChE;CAEA,MAAM,SAAgC;EACpC,OAAO,MAAM,QAAQ,QACnB,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CACvB,QAAQ,UAAU,MAAM,WAAW,YAAY,MAAM,WAAW,SAAS,CAAC,CAC1E,UAAU,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC,CAC5D;CACF;CAEA,MAAM,OAAO,OAAsC;EACjD,OAAO,MAAM,QAAQ,QACnB,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CACvB,UAAU,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC,CAAC,CACxD,MAAM,GAAG,KAAK,CACnB;CACF;CAEA,MAAc,QAAQ,KAAiB,OAAkC;EACvE,MAAM,MAAM,KAAK,IAAI,GAAG;EACxB,MAAM,KAAK,QAAQ;EACnB,MAAM,SAAS;EACf,MAAM,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EACzC,KAAK,KAAK,iBAAiB;EAC3B,IAAI;GACF,MAAM,KAAK,QAAQ,OAAO,GAAG;EAC/B,SAAS,OAAO;GACd,MAAM,SAAS;GACf,MAAM,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;GAC1C,MAAM,QAAQ,iBAAiB,QAAQ,MAAM,UAAU;GACvD,KAAK,MAAM,EAAE,KAAK,MAAM,GAAG,gBAAgB;EAC7C,UAAU;GACR,KAAK,WAAW;GAChB,KAAK,QAAQ,MAAM,CAAC,GAAG;GACvB,IAAI,MAAM,WAAW,UAAU;IAC7B,MAAM,SAAS;IACf,MAAM,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;IAC1C,KAAK,KAAK,mBAAmB;GAC/B;EACF;CACF;CAEA,IAAY,KAAqC;EAC/C,OAAO,KAAK,QAAQ,QAAQ,MAAM;GAAE,SAAS,IAAI;GAAS,OAAO,IAAI;EAAM,CAAC;CAC9E;CAEA,MAAc,SAA6B;EACzC,MAAM,WAAW,KAAK,QAAQ,IAAI,OAAO;EACzC,IAAI,UAAU;GACZ,SAAS,SAAS;GAClB,SAAS,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;GAC3C,SAAS,YAAY,KAAA;GACrB,SAAS,aAAa,KAAA;GACtB,SAAS,QAAQ,KAAA;GACjB,OAAO;EACT;EACA,MAAM,QAAoB;GAAE;GAAS,QAAQ;GAAU,2BAAU,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE;EAC1F,KAAK,QAAQ,IAAI,SAAS,KAAK;EAC/B,OAAO;CACT;CAEA,MAAc,UAAyB;EACrC,IAAI,KAAK,UAAU,KAAK,QAAQ,aAAa;GAC3C,KAAK,WAAW;GAChB;EACF;EACA,MAAM,IAAI,SAAe,YAAY;GACnC,KAAK,QAAQ,KAAK,OAAO;EAC3B,CAAC;EACD,MAAM,KAAK,QAAQ;CACrB;AACF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { StatusConfigModel } from "../models/status-config.mjs";
|
|
6
|
+
//#region src/capture/skip-checks.ts
|
|
7
|
+
/** Skip capture when another approved build already covers the same commit. */
|
|
8
|
+
async function hasApprovedBuildForSha(db, projectId, sha, excludeBuildId) {
|
|
9
|
+
return (await new BuildModel(db).list(projectId)).some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === "approved");
|
|
10
|
+
}
|
|
11
|
+
function findRowProvider(providers, row) {
|
|
12
|
+
return providers.find((p) => p.metadata.kind === row.provider);
|
|
13
|
+
}
|
|
14
|
+
function decodeRowConfig(provider, configJson) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(configJson);
|
|
17
|
+
provider.metadata.schema.parse(parsed);
|
|
18
|
+
return parsed;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function decodeRowToken(model, row) {
|
|
24
|
+
try {
|
|
25
|
+
return model.decryptToken(row);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function parseRow(model, row, providers) {
|
|
31
|
+
const provider = findRowProvider(providers, row);
|
|
32
|
+
if (!provider) return null;
|
|
33
|
+
const parsed = decodeRowConfig(provider, row.config);
|
|
34
|
+
if (parsed === null) return null;
|
|
35
|
+
const token = decodeRowToken(model, row);
|
|
36
|
+
if (token === null) return null;
|
|
37
|
+
return {
|
|
38
|
+
provider,
|
|
39
|
+
config: parsed,
|
|
40
|
+
token
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function checkRowMerged(parsed, sha, branch, logger) {
|
|
44
|
+
const adapter = parsed.provider.create({
|
|
45
|
+
config: parsed.config,
|
|
46
|
+
token: parsed.token,
|
|
47
|
+
logger
|
|
48
|
+
});
|
|
49
|
+
if (!adapter.isMerged) return false;
|
|
50
|
+
try {
|
|
51
|
+
return await adapter.isMerged({
|
|
52
|
+
sha,
|
|
53
|
+
branch
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Check one status-config row for an upstream merge. */
|
|
60
|
+
async function checkRow(model, row, providers, sha, branch, logger) {
|
|
61
|
+
const parsed = parseRow(model, row, providers);
|
|
62
|
+
if (!parsed) return false;
|
|
63
|
+
return await checkRowMerged(parsed, sha, branch, logger);
|
|
64
|
+
}
|
|
65
|
+
/** True when any configured provider reports the commit already merged. */
|
|
66
|
+
async function isAlreadyMerged(opts) {
|
|
67
|
+
if (opts.providers.length === 0) return false;
|
|
68
|
+
const model = new StatusConfigModel(opts.db, opts.secret);
|
|
69
|
+
const rows = await model.list(opts.projectId);
|
|
70
|
+
for (const row of rows) if (await checkRow(model, row, opts.providers, opts.sha, opts.branch, opts.logger)) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { hasApprovedBuildForSha, isAlreadyMerged };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=skip-checks.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skip-checks.mjs","names":[],"sources":["../../src/capture/skip-checks.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema/status-config.ts\";\n\n/** Skip capture when another approved build already covers the same commit. */\nexport async function hasApprovedBuildForSha(\n db: DatabaseAdapter,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n/** Inputs for checking whether a commit is already merged upstream. */\nexport interface MergeCheck {\n providers: GitHostProvider[];\n sha: string;\n branch: string;\n secret: string | undefined;\n db: DatabaseAdapter;\n projectId: string;\n logger?: Logger;\n}\n\ninterface ParsedRow {\n provider: GitHostProvider;\n config: unknown;\n token: string;\n}\n\nfunction findRowProvider(\n providers: GitHostProvider[],\n row: { provider: string },\n): GitHostProvider | undefined {\n return providers.find((p) => p.metadata.kind === row.provider);\n}\n\nfunction decodeRowConfig(provider: GitHostProvider, configJson: string): unknown {\n try {\n const parsed: unknown = JSON.parse(configJson);\n provider.metadata.schema.parse(parsed);\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction decodeRowToken(model: StatusConfigModel, row: ProjectStatusConfig): string | null {\n try {\n return model.decryptToken(row);\n } catch {\n return null;\n }\n}\n\nfunction parseRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n): ParsedRow | null {\n const provider = findRowProvider(providers, row);\n if (!provider) {\n return null;\n }\n const parsed = decodeRowConfig(provider, row.config);\n if (parsed === null) {\n return null;\n }\n const token = decodeRowToken(model, row);\n if (token === null) {\n return null;\n }\n return { provider, config: parsed, token };\n}\n\nasync function checkRowMerged(\n parsed: ParsedRow,\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const adapter = parsed.provider.create({ config: parsed.config, token: parsed.token, logger });\n if (!adapter.isMerged) {\n return false;\n }\n try {\n return await adapter.isMerged({ sha, branch });\n } catch {\n // Ignore provider errors — do not skip on failure\n return false;\n }\n}\n\n/** Check one status-config row for an upstream merge. */\nasync function checkRow(\n model: StatusConfigModel,\n row: ProjectStatusConfig,\n providers: GitHostProvider[],\n sha: string,\n branch: string,\n logger?: Logger,\n): Promise<boolean> {\n const parsed = parseRow(model, row, providers);\n if (!parsed) {\n return false;\n }\n return await checkRowMerged(parsed, sha, branch, logger);\n}\n\n/** True when any configured provider reports the commit already merged. */\nexport async function isAlreadyMerged(opts: MergeCheck): Promise<boolean> {\n if (opts.providers.length === 0) {\n return false;\n }\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n if (await checkRow(model, row, opts.providers, opts.sha, opts.branch, opts.logger)) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";;;;;;;AAQA,eAAsB,uBACpB,IACA,WACA,KACA,gBACkB;CAElB,QAAO,MADc,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,SAAS,EAAA,CACxC,MAAM,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,kBAAkB,EAAE,WAAW,UAAU;AAClG;AAmBA,SAAS,gBACP,WACA,KAC6B;CAC7B,OAAO,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;AAC/D;AAEA,SAAS,gBAAgB,UAA2B,YAA6B;CAC/E,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,UAAU;EAC7C,SAAS,SAAS,OAAO,MAAM,MAAM;EACrC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,OAA0B,KAAyC;CACzF,IAAI;EACF,OAAO,MAAM,aAAa,GAAG;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,SACP,OACA,KACA,WACkB;CAClB,MAAM,WAAW,gBAAgB,WAAW,GAAG;CAC/C,IAAI,CAAC,UACH,OAAO;CAET,MAAM,SAAS,gBAAgB,UAAU,IAAI,MAAM;CACnD,IAAI,WAAW,MACb,OAAO;CAET,MAAM,QAAQ,eAAe,OAAO,GAAG;CACvC,IAAI,UAAU,MACZ,OAAO;CAET,OAAO;EAAE;EAAU,QAAQ;EAAQ;CAAM;AAC3C;AAEA,eAAe,eACb,QACA,KACA,QACA,QACkB;CAClB,MAAM,UAAU,OAAO,SAAS,OAAO;EAAE,QAAQ,OAAO;EAAQ,OAAO,OAAO;EAAO;CAAO,CAAC;CAC7F,IAAI,CAAC,QAAQ,UACX,OAAO;CAET,IAAI;EACF,OAAO,MAAM,QAAQ,SAAS;GAAE;GAAK;EAAO,CAAC;CAC/C,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,eAAe,SACb,OACA,KACA,WACA,KACA,QACA,QACkB;CAClB,MAAM,SAAS,SAAS,OAAO,KAAK,SAAS;CAC7C,IAAI,CAAC,QACH,OAAO;CAET,OAAO,MAAM,eAAe,QAAQ,KAAK,QAAQ,MAAM;AACzD;;AAGA,eAAsB,gBAAgB,MAAoC;CACxE,IAAI,KAAK,UAAU,WAAW,GAC5B,OAAO;CAET,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,MAAM;CACxD,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,SAAS;CAC5C,KAAK,MAAM,OAAO,MAEhB,IAAI,MAAM,SAAS,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,KAAK,QAAQ,KAAK,MAAM,GAC/E,OAAO;CAGX,OAAO;AACT"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { storybookDir, storybookZipPath } from "../paths.mjs";
|
|
5
|
+
import { createWriteStream } from "node:fs";
|
|
6
|
+
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
7
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { Parse } from "unzipper";
|
|
10
|
+
//#region src/capture/statics.ts
|
|
11
|
+
/**
|
|
12
|
+
* Shared Storybook statics handling: unzip a stored build into a scratch
|
|
13
|
+
* directory and persist the extracted files back to storage. Used by the
|
|
14
|
+
* capture orchestrator and by the upload path for opt-in inline extraction
|
|
15
|
+
* (`maxInlineUnzipSize`); both write identical keys, so either may run first.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Extract a build's stored zip into a dedicated scratch directory.
|
|
19
|
+
*
|
|
20
|
+
* @param storage - Storage holding the uploaded zip.
|
|
21
|
+
* @param scratchDir - Root under which per-build scratch dirs are created.
|
|
22
|
+
* @param projectId - Owning project id.
|
|
23
|
+
* @param buildId - Build whose zip to extract.
|
|
24
|
+
* @returns The extracted Storybook directory.
|
|
25
|
+
*/
|
|
26
|
+
async function extractStorybookToScratch(storage, scratchDir, projectId, buildId) {
|
|
27
|
+
const targetDir = join(scratchDir, projectId, "builds", buildId, "storybook");
|
|
28
|
+
const root = await prepareTargetDir(targetDir);
|
|
29
|
+
const source = await storage.readStream(storybookZipPath(projectId, buildId));
|
|
30
|
+
const writes = [];
|
|
31
|
+
const parseError = await runParser(root, source, writes);
|
|
32
|
+
if (parseError) throw parseError;
|
|
33
|
+
await awaitWrites(writes);
|
|
34
|
+
return targetDir;
|
|
35
|
+
}
|
|
36
|
+
/** Reset a scratch target directory, returning its resolved root. */
|
|
37
|
+
async function prepareTargetDir(targetDir) {
|
|
38
|
+
await rm(targetDir, {
|
|
39
|
+
recursive: true,
|
|
40
|
+
force: true
|
|
41
|
+
});
|
|
42
|
+
await mkdir(targetDir, { recursive: true });
|
|
43
|
+
return resolve(targetDir);
|
|
44
|
+
}
|
|
45
|
+
/** Await streamed zip writes, throwing the first write failure. */
|
|
46
|
+
async function awaitWrites(writes) {
|
|
47
|
+
const writeError = (await Promise.allSettled(writes)).find((outcome) => outcome.status === "rejected");
|
|
48
|
+
if (writeError) throw writeError.reason;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist an extracted Storybook directory to storage for published serving.
|
|
52
|
+
*
|
|
53
|
+
* @param storage - Destination storage adapter.
|
|
54
|
+
* @param sourceDir - Extracted Storybook directory.
|
|
55
|
+
* @param projectId - Owning project id.
|
|
56
|
+
* @param buildId - Build the files belong to.
|
|
57
|
+
*/
|
|
58
|
+
async function persistStorybookStatics(storage, sourceDir, projectId, buildId) {
|
|
59
|
+
const root = resolve(sourceDir);
|
|
60
|
+
const destinationPrefix = storybookDir(projectId, buildId);
|
|
61
|
+
const files = await walkFiles(root);
|
|
62
|
+
await Promise.all(files.map(async (file) => {
|
|
63
|
+
const rel = relative(root, file);
|
|
64
|
+
await storage.write(`${destinationPrefix}/${rel}`, await readFile(file));
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
function blockedTarget(root, entryName) {
|
|
68
|
+
const candidate = resolve(join(root, entryName));
|
|
69
|
+
return candidate !== root && !candidate.startsWith(root + sep);
|
|
70
|
+
}
|
|
71
|
+
/** Pipe one zip entry to disk, tracking the write for later aggregation. */
|
|
72
|
+
function handleEntry(root, entry, writes, fail) {
|
|
73
|
+
if (blockedTarget(root, entry.path)) {
|
|
74
|
+
entry.autodrain();
|
|
75
|
+
fail(/* @__PURE__ */ new Error(`Blocked path traversal in uploaded Storybook: ${entry.path}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (entry.type === "Directory") {
|
|
79
|
+
entry.autodrain();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
writeEntry(root, entry, writes);
|
|
83
|
+
}
|
|
84
|
+
/** Stream one zip entry to disk, tracking the write for later aggregation. */
|
|
85
|
+
function writeEntry(root, entry, writes) {
|
|
86
|
+
const target = join(root, entry.path);
|
|
87
|
+
const done = (async () => {
|
|
88
|
+
await mkdir(dirname(target), { recursive: true });
|
|
89
|
+
await pipeline(entry, createWriteStream(target));
|
|
90
|
+
})();
|
|
91
|
+
writes.push(done);
|
|
92
|
+
done.catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
/** Run the unzipper parser, resolving with the first parse error (or null). */
|
|
95
|
+
function runParser(root, source, writes) {
|
|
96
|
+
return new Promise((settle) => {
|
|
97
|
+
const parser = Parse();
|
|
98
|
+
const fail = (error) => {
|
|
99
|
+
source.destroy();
|
|
100
|
+
settle(error);
|
|
101
|
+
};
|
|
102
|
+
parser.on("entry", (entry) => {
|
|
103
|
+
handleEntry(root, entry, writes, fail);
|
|
104
|
+
});
|
|
105
|
+
parser.on("error", fail);
|
|
106
|
+
parser.on("close", () => {
|
|
107
|
+
settle(null);
|
|
108
|
+
});
|
|
109
|
+
source.on("error", fail);
|
|
110
|
+
source.pipe(parser);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function walkFiles(dir) {
|
|
114
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
115
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
116
|
+
const full = join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) return await walkFiles(full);
|
|
118
|
+
return [full];
|
|
119
|
+
}))).flat();
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { extractStorybookToScratch, persistStorybookStatics };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=statics.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statics.mjs","names":[],"sources":["../../src/capture/statics.ts"],"sourcesContent":["import { createWriteStream } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { Parse, type Entry } from \"unzipper\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\n\n/**\n * Shared Storybook statics handling: unzip a stored build into a scratch\n * directory and persist the extracted files back to storage. Used by the\n * capture orchestrator and by the upload path for opt-in inline extraction\n * (`maxInlineUnzipSize`); both write identical keys, so either may run first.\n */\n\n/**\n * Extract a build's stored zip into a dedicated scratch directory.\n *\n * @param storage - Storage holding the uploaded zip.\n * @param scratchDir - Root under which per-build scratch dirs are created.\n * @param projectId - Owning project id.\n * @param buildId - Build whose zip to extract.\n * @returns The extracted Storybook directory.\n */\nexport async function extractStorybookToScratch(\n storage: StorageAdapter,\n scratchDir: string,\n projectId: string,\n buildId: string,\n): Promise<string> {\n const targetDir = join(scratchDir, projectId, \"builds\", buildId, \"storybook\");\n const root = await prepareTargetDir(targetDir);\n const source = await storage.readStream(storybookZipPath(projectId, buildId));\n const writes: Promise<void>[] = [];\n const parseError = await runParser(root, source, writes);\n if (parseError) {\n throw parseError;\n }\n await awaitWrites(writes);\n return targetDir;\n}\n\n/** Reset a scratch target directory, returning its resolved root. */\nasync function prepareTargetDir(targetDir: string): Promise<string> {\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n return resolve(targetDir);\n}\n\n/** Await streamed zip writes, throwing the first write failure. */\nasync function awaitWrites(writes: Promise<void>[]): Promise<void> {\n const outcomes = await Promise.allSettled(writes);\n const writeError = outcomes.find(\n (outcome): outcome is PromiseRejectedResult => outcome.status === \"rejected\",\n );\n if (writeError) {\n throw writeError.reason;\n }\n}\n\n/**\n * Persist an extracted Storybook directory to storage for published serving.\n *\n * @param storage - Destination storage adapter.\n * @param sourceDir - Extracted Storybook directory.\n * @param projectId - Owning project id.\n * @param buildId - Build the files belong to.\n */\nexport async function persistStorybookStatics(\n storage: StorageAdapter,\n sourceDir: string,\n projectId: string,\n buildId: string,\n): Promise<void> {\n const root = resolve(sourceDir);\n const destinationPrefix = storybookDir(projectId, buildId);\n const files = await walkFiles(root);\n await Promise.all(\n files.map(async (file) => {\n const rel = relative(root, file);\n await storage.write(`${destinationPrefix}/${rel}`, await readFile(file));\n }),\n );\n}\n\nfunction blockedTarget(root: string, entryName: string): boolean {\n const candidate = resolve(join(root, entryName));\n return candidate !== root && !candidate.startsWith(root + sep);\n}\n\n/** Pipe one zip entry to disk, tracking the write for later aggregation. */\nfunction handleEntry(\n root: string,\n entry: Entry,\n writes: Promise<void>[],\n fail: (error: unknown) => void,\n): void {\n if (blockedTarget(root, entry.path)) {\n entry.autodrain();\n fail(new Error(`Blocked path traversal in uploaded Storybook: ${entry.path}`));\n return;\n }\n if (entry.type === \"Directory\") {\n entry.autodrain();\n return;\n }\n writeEntry(root, entry, writes);\n}\n\n/** Stream one zip entry to disk, tracking the write for later aggregation. */\nfunction writeEntry(root: string, entry: Entry, writes: Promise<void>[]): void {\n const target = join(root, entry.path);\n const done = (async (): Promise<void> => {\n await mkdir(dirname(target), { recursive: true });\n await pipeline(entry, createWriteStream(target));\n })();\n writes.push(done);\n done.catch(() => {}); // Intentionally empty — aggregated via allSettled by the caller\n}\n\n/** Run the unzipper parser, resolving with the first parse error (or null). */\n// oxlint-disable-next-line typescript/promise-function-async -- executor-style promise wrapper around events\nfunction runParser(\n root: string,\n source: import(\"node:stream\").Readable,\n writes: Promise<void>[],\n): Promise<unknown> {\n return new Promise((settle) => {\n const parser = Parse();\n const fail = (error: unknown): void => {\n source.destroy();\n settle(error);\n };\n parser.on(\"entry\", (entry: Entry) => {\n handleEntry(root, entry, writes, fail);\n });\n parser.on(\"error\", fail);\n parser.on(\"close\", () => {\n settle(null);\n });\n source.on(\"error\", fail);\n source.pipe(parser);\n });\n}\n\nasync function walkFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n return await walkFiles(full);\n }\n return [full];\n }),\n );\n return nested.flat();\n}\n"],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { StatusConfigModel } from "../models/status-config.mjs";
|
|
5
5
|
import { buildCommentMarkdown } from "../adapter/git-host/helpers.mjs";
|
|
6
6
|
//#region src/capture/status-fanout.ts
|
|
7
|
+
/** Post build check statuses (and review comments) to every configured git provider. */
|
|
7
8
|
async function postStatusesForBuild(opts) {
|
|
8
9
|
if (opts.providers.length === 0) return;
|
|
9
10
|
const model = new StatusConfigModel(opts.db, opts.secret);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-fanout.mjs","names":[],"sources":["../../src/capture/status-fanout.ts"],"sourcesContent":["import type { Logger } from \"pino\";\
|
|
1
|
+
{"version":3,"file":"status-fanout.mjs","names":[],"sources":["../../src/capture/status-fanout.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { buildCommentMarkdown } from \"../adapters/git-host/helpers.ts\";\nimport type { CheckStatus, GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { Project } from \"../schema/project.ts\";\n\n/** Post build check statuses (and review comments) to every configured git provider. */\nasync function postStatusesForBuild(opts: {\n db: DatabaseAdapter;\n project: Project;\n sha: string;\n status: CheckStatus;\n url: string;\n providers: GitHostProvider[];\n secret: string | undefined;\n logger?: Logger;\n}): Promise<void> {\n if (opts.providers.length === 0) {\n return;\n }\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.project.id);\n const ctx = `storyshelf/${opts.project.slug}`;\n await Promise.allSettled(\n rows.map(async (row) => {\n const provider = opts.providers.find((p) => p.metadata.kind === row.provider);\n if (!provider) {\n opts.logger?.warn({ provider: row.provider }, \"no provider registered for status config\");\n return;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(row.config);\n provider.metadata.schema.parse(parsed);\n } catch (error: unknown) {\n opts.logger?.warn({ err: error, provider: row.provider }, \"invalid status config\");\n return;\n }\n let token: string;\n try {\n token = model.decryptToken(row);\n } catch (error: unknown) {\n opts.logger?.warn({ err: error }, \"failed to decrypt status token\");\n return;\n }\n const adapter = provider.create({ config: parsed, token, logger: opts.logger });\n await adapter.setStatus({\n context: ctx,\n gitSha: opts.sha,\n status: opts.status,\n url: opts.url,\n });\n if (adapter.upsertComment) {\n const markdown = buildCommentMarkdown(opts.status, opts.url, ctx);\n await adapter\n .upsertComment({ sha: opts.sha, url: opts.url, status: opts.status, markdown })\n .catch((error: unknown) => {\n opts.logger?.warn({ err: error, provider: row.provider }, \"failed to upsert comment\");\n });\n }\n }),\n );\n}\n\nexport { postStatusesForBuild };\n"],"mappings":";;;;;;;AAQA,eAAe,qBAAqB,MASlB;CAChB,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,MAAM,QAAQ,IAAI,kBAAkB,KAAK,IAAI,KAAK,MAAM;CACxD,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,QAAQ,EAAE;CAC7C,MAAM,MAAM,cAAc,KAAK,QAAQ;CACvC,MAAM,QAAQ,WACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,WAAW,KAAK,UAAU,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI,QAAQ;EAC5E,IAAI,CAAC,UAAU;GACb,KAAK,QAAQ,KAAK,EAAE,UAAU,IAAI,SAAS,GAAG,0CAA0C;GACxF;EACF;EACA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI,MAAM;GAC9B,SAAS,SAAS,OAAO,MAAM,MAAM;EACvC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK;IAAE,KAAK;IAAO,UAAU,IAAI;GAAS,GAAG,uBAAuB;GACjF;EACF;EACA,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,aAAa,GAAG;EAChC,SAAS,OAAgB;GACvB,KAAK,QAAQ,KAAK,EAAE,KAAK,MAAM,GAAG,gCAAgC;GAClE;EACF;EACA,MAAM,UAAU,SAAS,OAAO;GAAE,QAAQ;GAAQ;GAAO,QAAQ,KAAK;EAAO,CAAC;EAC9E,MAAM,QAAQ,UAAU;GACtB,SAAS;GACT,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,KAAK,KAAK;EACZ,CAAC;EACD,IAAI,QAAQ,eAAe;GACzB,MAAM,WAAW,qBAAqB,KAAK,QAAQ,KAAK,KAAK,GAAG;GAChE,MAAM,QACH,cAAc;IAAE,KAAK,KAAK;IAAK,KAAK,KAAK;IAAK,QAAQ,KAAK;IAAQ;GAAS,CAAC,CAAC,CAC9E,OAAO,UAAmB;IACzB,KAAK,QAAQ,KAAK;KAAE,KAAK;KAAO,UAAU,IAAI;IAAS,GAAG,0BAA0B;GACtF,CAAC;EACL;CACF,CAAC,CACH;AACF"}
|
|
@@ -2,6 +2,7 @@ import __tsdown_shims_path from 'node:path';
|
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { StoryEntry, StorySourceAdapter } from "./adapter.mjs";
|
|
4
4
|
//#region src/capture/storybook.d.ts
|
|
5
|
+
/** Discovers stories from a built Storybook via its index file. */
|
|
5
6
|
declare class StorybookAdapter implements StorySourceAdapter {
|
|
6
7
|
readonly name = "storybook";
|
|
7
8
|
readonly screenshotSelector = "#storybook-root";
|
|
@@ -11,6 +11,7 @@ function mergeParameters(entry) {
|
|
|
11
11
|
};
|
|
12
12
|
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
13
13
|
}
|
|
14
|
+
/** Discovers stories from a built Storybook via its index file. */
|
|
14
15
|
var StorybookAdapter = class {
|
|
15
16
|
name = "storybook";
|
|
16
17
|
screenshotSelector = "#storybook-root";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/capture/storybook.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\
|
|
1
|
+
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/capture/storybook.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { StoryEntry, StoryParameters, StorySourceAdapter } from \"./adapter.ts\";\n\ninterface StorybookIndex {\n v: number;\n entries: Record<\n string,\n {\n id: string;\n name: string;\n title: string;\n importPath?: string;\n tags?: string[];\n type: string;\n subtype?: string;\n parameters?: { chromatic?: StoryParameters; storyshelf?: StoryParameters };\n }\n >;\n}\n\nfunction mergeParameters(entry: {\n parameters?: { chromatic?: StoryParameters; storyshelf?: StoryParameters };\n}): StoryParameters | undefined {\n const merged: StoryParameters = {\n ...entry.parameters?.chromatic,\n ...entry.parameters?.storyshelf,\n };\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\n/** Discovers stories from a built Storybook via its index file. */\nexport class StorybookAdapter implements StorySourceAdapter {\n readonly name = \"storybook\";\n readonly screenshotSelector = \"#storybook-root\";\n\n async discover(source: string): Promise<StoryEntry[]> {\n const index = await this.readIndex(source);\n return Object.values(index.entries)\n .filter((entry) => entry.type !== \"docs\" && entry.subtype !== \"test\")\n .map((entry) => ({\n id: entry.id,\n title: entry.title,\n name: entry.name,\n importPath: entry.importPath,\n tags: entry.tags,\n type: entry.type === \"docs\" ? \"docs\" : \"story\",\n parameters: mergeParameters(entry),\n }));\n }\n\n // eslint-disable-next-line class-methods-use-this\n buildUrl(baseUrl: string, storyId: string): string {\n return `${baseUrl}/iframe.html?id=${encodeURIComponent(storyId)}&viewMode=story`;\n }\n\n // eslint-disable-next-line class-methods-use-this\n private async readIndex(source: string): Promise<StorybookIndex> {\n const candidates: string[] = [\"stories.json\", \"index.json\"];\n const results = await Promise.all(\n candidates.map(async (name) => {\n try {\n const raw = await readFile(join(source, name), \"utf8\");\n return JSON.parse(raw) as StorybookIndex;\n } catch {\n return null;\n }\n }),\n );\n const found = results.find((index) => index !== null);\n if (found) {\n return found;\n }\n throw new Error(`No Storybook index found in ${source}`);\n }\n}\n"],"mappings":";;;;;;AAqBA,SAAS,gBAAgB,OAEO;CAC9B,MAAM,SAA0B;EAC9B,GAAG,MAAM,YAAY;EACrB,GAAG,MAAM,YAAY;CACvB;CACA,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,SAAS,KAAA;AACnD;;AAGA,IAAa,mBAAb,MAA4D;CAC1D,OAAgB;CAChB,qBAA8B;CAE9B,MAAM,SAAS,QAAuC;EACpD,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM;EACzC,OAAO,OAAO,OAAO,MAAM,OAAO,CAAC,CAChC,QAAQ,UAAU,MAAM,SAAS,UAAU,MAAM,YAAY,MAAM,CAAC,CACpE,KAAK,WAAW;GACf,IAAI,MAAM;GACV,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,YAAY,MAAM;GAClB,MAAM,MAAM;GACZ,MAAM,MAAM,SAAS,SAAS,SAAS;GACvC,YAAY,gBAAgB,KAAK;EACnC,EAAE;CACN;CAGA,SAAS,SAAiB,SAAyB;EACjD,OAAO,GAAG,QAAQ,kBAAkB,mBAAmB,OAAO,EAAE;CAClE;CAGA,MAAc,UAAU,QAAyC;EAY/D,MAAM,SAAQ,MAVQ,QAAQ,IAC5B,CAF4B,gBAAgB,YAEnC,CAAC,CAAC,IAAI,OAAO,SAAS;GAC7B,IAAI;IACF,MAAM,MAAM,MAAM,SAAS,KAAK,QAAQ,IAAI,GAAG,MAAM;IACrD,OAAO,KAAK,MAAM,GAAG;GACvB,QAAQ;IACN,OAAO;GACT;EACF,CAAC,CACH,EAAA,CACsB,MAAM,UAAU,UAAU,IAAI;EACpD,IAAI,OACF,OAAO;EAET,MAAM,IAAI,MAAM,+BAA+B,QAAQ;CACzD;AACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DEFAULT_VIEWPORTS, StoryEntry, StoryParameters, StorySourceAdapter, Viewport, isDisabledStory, isFlakyStory } from "./capture/adapter.mjs";
|
|
4
|
+
import { CaptureJobOptions, executeCaptureJob } from "./capture/orchestrator.mjs";
|
|
5
|
+
import { CaptureContext, persistCapture } from "./capture/pipeline.mjs";
|
|
6
|
+
import { InMemoryCaptureQueue, InMemoryCaptureQueueOptions } from "./capture/queue.mjs";
|
|
7
|
+
import { StorybookAdapter } from "./capture/storybook.mjs";
|
|
8
|
+
export { type CaptureContext, type CaptureJobOptions, DEFAULT_VIEWPORTS, InMemoryCaptureQueue, type InMemoryCaptureQueueOptions, type StoryEntry, type StoryParameters, type StorySourceAdapter, StorybookAdapter, type Viewport, executeCaptureJob, isDisabledStory, isFlakyStory, persistCapture };
|
package/dist/capture.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from "./capture/adapter.mjs";
|
|
5
|
+
import { persistCapture } from "./capture/pipeline.mjs";
|
|
6
|
+
import { StorybookAdapter } from "./capture/storybook.mjs";
|
|
7
|
+
import { executeCaptureJob } from "./capture/orchestrator.mjs";
|
|
8
|
+
import { InMemoryCaptureQueue } from "./capture/queue.mjs";
|
|
9
|
+
export { DEFAULT_VIEWPORTS, InMemoryCaptureQueue, StorybookAdapter, executeCaptureJob, isDisabledStory, isFlakyStory, persistCapture };
|