@storyshelf/core 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -32
- package/dist/adapter/capture-queue.d.mts +44 -2
- package/dist/adapter/capture-runner.d.mts +127 -10
- package/dist/adapter/capture-runner.mjs +5 -12
- package/dist/adapter/database.d.mts +27 -8
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/design.d.mts +85 -0
- package/dist/adapter/design.mjs +28 -0
- package/dist/adapter/design.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +3 -10
- package/dist/adapter/notifier.d.mts +6 -3
- package/dist/adapter/notifier.mjs +5 -1
- package/dist/adapter/oauth.d.mts +52 -0
- package/dist/adapter/setup.d.mts +4 -5
- package/dist/adapter/setup.mjs +2 -1
- package/dist/adapter/setup.mjs.map +1 -1
- package/dist/adapter/webhook-events.mjs +1 -1
- package/dist/adapters/capture-target-offers.d.mts +28 -0
- package/dist/adapters/capture-target-offers.mjs +58 -0
- package/dist/adapters/capture-target-offers.mjs.map +1 -0
- package/dist/adapters/capture-target.d.mts +71 -0
- package/dist/adapters/capture-target.mjs +76 -0
- package/dist/adapters/capture-target.mjs.map +1 -0
- package/dist/adapters/notifier/build-comment.d.mts +24 -0
- package/dist/adapters/notifier/build-comment.mjs +24 -0
- package/dist/adapters/notifier/build-comment.mjs.map +1 -0
- package/dist/adapters/notifier/build-state.d.mts +99 -0
- package/dist/adapters/notifier/build-state.mjs +218 -0
- package/dist/adapters/notifier/build-state.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +4 -2
- package/dist/adapters/notifier/events.mjs +31 -18
- package/dist/adapters/notifier/events.mjs.map +1 -1
- package/dist/adapters/notifier/provider.d.mts +53 -7
- package/dist/adapters/notifier/provider.mjs +12 -0
- package/dist/adapters/notifier/provider.mjs.map +1 -0
- package/dist/adapters/notifier/state.d.mts +24 -0
- package/dist/adapters/notifier/state.mjs +66 -0
- package/dist/adapters/notifier/state.mjs.map +1 -0
- package/dist/adapters/notifier/types.d.mts +51 -2
- package/dist/adapters/remote-browser.d.mts +87 -0
- package/dist/adapters/remote-browser.mjs +103 -0
- package/dist/adapters/remote-browser.mjs.map +1 -0
- package/dist/adapters/render-runs.d.mts +25 -0
- package/dist/adapters/render-runs.mjs +41 -0
- package/dist/adapters/render-runs.mjs.map +1 -0
- package/dist/adapters/setup-runner.mjs +2 -4
- package/dist/adapters/setup-runner.mjs.map +1 -1
- package/dist/adapters/sources.d.mts +12 -0
- package/dist/adapters/sources.mjs +20 -0
- package/dist/adapters/sources.mjs.map +1 -0
- package/dist/adapters/validate.mjs +25 -18
- package/dist/adapters/validate.mjs.map +1 -1
- package/dist/ai.d.mts +105 -0
- package/dist/ai.mjs +19 -0
- package/dist/ai.mjs.map +1 -0
- package/dist/auth.d.mts +8 -1
- package/dist/auth.mjs +14 -1
- package/dist/auth.mjs.map +1 -0
- package/dist/capture/a11y/axe-source.d.mts +18 -0
- package/dist/capture/a11y/axe-source.mjs +37 -0
- package/dist/capture/a11y/axe-source.mjs.map +1 -0
- package/dist/capture/a11y/in-page.d.mts +27 -0
- package/dist/capture/a11y/in-page.mjs +60 -0
- package/dist/capture/a11y/in-page.mjs.map +1 -0
- package/dist/capture/a11y/map.d.mts +24 -0
- package/dist/capture/a11y/map.mjs +48 -0
- package/dist/capture/a11y/map.mjs.map +1 -0
- package/dist/capture/a11y/options.d.mts +23 -0
- package/dist/capture/a11y/options.mjs +75 -0
- package/dist/capture/a11y/options.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +41 -3
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +2 -0
- package/dist/capture/affected.mjs +13 -4
- package/dist/capture/affected.mjs.map +1 -1
- package/dist/capture/attempt-logger.mjs +84 -0
- package/dist/capture/attempt-logger.mjs.map +1 -0
- package/dist/capture/browsers.d.mts +35 -0
- package/dist/capture/browsers.mjs +67 -0
- package/dist/capture/browsers.mjs.map +1 -0
- package/dist/capture/build-state/deliver.d.mts +32 -0
- package/dist/capture/build-state/deliver.mjs +118 -0
- package/dist/capture/build-state/deliver.mjs.map +1 -0
- package/dist/capture/build-state/index.d.mts +8 -0
- package/dist/capture/build-state/notify-job.d.mts +56 -0
- package/dist/capture/build-state/notify-job.mjs +133 -0
- package/dist/capture/build-state/notify-job.mjs.map +1 -0
- package/dist/capture/build-state/publisher.d.mts +23 -0
- package/dist/capture/build-state/publisher.mjs +107 -0
- package/dist/capture/build-state/publisher.mjs.map +1 -0
- package/dist/capture/build-state/resume.d.mts +9 -0
- package/dist/capture/build-state/resume.mjs +47 -0
- package/dist/capture/build-state/resume.mjs.map +1 -0
- package/dist/capture/build-state/types.d.mts +49 -0
- package/dist/capture/catalog/catalogue.d.mts +22 -0
- package/dist/capture/catalog/catalogue.mjs +53 -0
- package/dist/capture/catalog/catalogue.mjs.map +1 -0
- package/dist/capture/catalog/components.d.mts +23 -0
- package/dist/capture/catalog/components.mjs +55 -0
- package/dist/capture/catalog/components.mjs.map +1 -0
- package/dist/capture/catalog/history.d.mts +30 -0
- package/dist/capture/catalog/history.mjs +51 -0
- package/dist/capture/catalog/history.mjs.map +1 -0
- package/dist/capture/catalog/hook.d.mts +14 -0
- package/dist/capture/catalog/hook.mjs +19 -0
- package/dist/capture/catalog/hook.mjs.map +1 -0
- package/dist/capture/catalog/index.d.mts +11 -0
- package/dist/capture/catalog/load.d.mts +26 -0
- package/dist/capture/catalog/load.mjs +49 -0
- package/dist/capture/catalog/load.mjs.map +1 -0
- package/dist/capture/catalog/parse.d.mts +15 -0
- package/dist/capture/catalog/parse.mjs +56 -0
- package/dist/capture/catalog/parse.mjs.map +1 -0
- package/dist/capture/catalog/read.d.mts +12 -0
- package/dist/capture/catalog/read.mjs +33 -0
- package/dist/capture/catalog/read.mjs.map +1 -0
- package/dist/capture/catalog/types.d.mts +33 -0
- package/dist/capture/consumer/capture-handler.d.mts +58 -0
- package/dist/capture/consumer/capture-handler.mjs +118 -0
- package/dist/capture/consumer/capture-handler.mjs.map +1 -0
- package/dist/capture/consumer/idle.d.mts +17 -0
- package/dist/capture/consumer/idle.mjs +25 -0
- package/dist/capture/consumer/idle.mjs.map +1 -0
- package/dist/capture/consumer/index.d.mts +13 -0
- package/dist/capture/consumer/index.mjs +139 -0
- package/dist/capture/consumer/index.mjs.map +1 -0
- package/dist/capture/consumer/loop.mjs +110 -0
- package/dist/capture/consumer/loop.mjs.map +1 -0
- package/dist/capture/consumer/metrics.d.mts +41 -0
- package/dist/capture/consumer/metrics.mjs +38 -0
- package/dist/capture/consumer/metrics.mjs.map +1 -0
- package/dist/capture/consumer/retry.d.mts +33 -0
- package/dist/capture/consumer/retry.mjs +34 -0
- package/dist/capture/consumer/retry.mjs.map +1 -0
- package/dist/capture/consumer/run-job.mjs +195 -0
- package/dist/capture/consumer/run-job.mjs.map +1 -0
- package/dist/capture/consumer/startup.mjs +38 -0
- package/dist/capture/consumer/startup.mjs.map +1 -0
- package/dist/capture/consumer/types.d.mts +97 -0
- package/dist/capture/crash-failure.d.mts +25 -0
- package/dist/capture/crash-failure.mjs +70 -0
- package/dist/capture/crash-failure.mjs.map +1 -0
- package/dist/capture/db-queue/cadence.d.mts +32 -0
- package/dist/capture/db-queue/cadence.mjs +38 -0
- package/dist/capture/db-queue/cadence.mjs.map +1 -0
- package/dist/capture/db-queue/claim.d.mts +9 -0
- package/dist/capture/db-queue/claim.mjs +132 -0
- package/dist/capture/db-queue/claim.mjs.map +1 -0
- package/dist/capture/db-queue/clock.mjs +12 -0
- package/dist/capture/db-queue/clock.mjs.map +1 -0
- package/dist/capture/db-queue/codec.mjs +76 -0
- package/dist/capture/db-queue/codec.mjs.map +1 -0
- package/dist/capture/db-queue/index.d.mts +20 -0
- package/dist/capture/db-queue/index.mjs +89 -0
- package/dist/capture/db-queue/index.mjs.map +1 -0
- package/dist/capture/db-queue/keeper.d.mts +36 -0
- package/dist/capture/db-queue/keeper.mjs +92 -0
- package/dist/capture/db-queue/keeper.mjs.map +1 -0
- package/dist/capture/db-queue/lease.mjs +46 -0
- package/dist/capture/db-queue/lease.mjs.map +1 -0
- package/dist/capture/db-queue/lifecycle.mjs +36 -0
- package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
- package/dist/capture/db-queue/operations.mjs +94 -0
- package/dist/capture/db-queue/operations.mjs.map +1 -0
- package/dist/capture/db-queue/poll-loop.d.mts +30 -0
- package/dist/capture/db-queue/poll-loop.mjs +89 -0
- package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
- package/dist/capture/db-queue/poll.d.mts +10 -0
- package/dist/capture/db-queue/poll.mjs +69 -0
- package/dist/capture/db-queue/poll.mjs.map +1 -0
- package/dist/capture/db-queue/timers.d.mts +12 -0
- package/dist/capture/db-queue/timers.mjs +37 -0
- package/dist/capture/db-queue/timers.mjs.map +1 -0
- package/dist/capture/db-queue/types.d.mts +83 -0
- package/dist/capture/db-queue/types.mjs +12 -0
- package/dist/capture/db-queue/types.mjs.map +1 -0
- package/dist/capture/db-queue/wake.mjs +48 -0
- package/dist/capture/db-queue/wake.mjs.map +1 -0
- package/dist/capture/design.mjs +39 -0
- package/dist/capture/design.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +23 -3
- package/dist/capture/diff-record.mjs.map +1 -1
- package/dist/capture/dispatch.d.mts +12 -7
- package/dist/capture/dispatch.mjs +86 -107
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/enrich.mjs +60 -0
- package/dist/capture/enrich.mjs.map +1 -0
- package/dist/capture/failure-sets.mjs +22 -0
- package/dist/capture/failure-sets.mjs.map +1 -0
- package/dist/capture/fingerprint.d.mts +42 -0
- package/dist/capture/fingerprint.mjs +83 -0
- package/dist/capture/fingerprint.mjs.map +1 -0
- package/dist/capture/forked/channel.d.mts +25 -0
- package/dist/capture/forked/child.d.mts +34 -0
- package/dist/capture/forked/child.mjs +202 -0
- package/dist/capture/forked/child.mjs.map +1 -0
- package/dist/capture/forked/crash-tracker.mjs +23 -0
- package/dist/capture/forked/crash-tracker.mjs.map +1 -0
- package/dist/capture/forked/decision.d.mts +26 -0
- package/dist/capture/forked/decision.mjs +27 -0
- package/dist/capture/forked/decision.mjs.map +1 -0
- package/dist/capture/forked/exit.mjs +12 -0
- package/dist/capture/forked/exit.mjs.map +1 -0
- package/dist/capture/forked/fork.d.mts +31 -0
- package/dist/capture/forked/fork.mjs +93 -0
- package/dist/capture/forked/fork.mjs.map +1 -0
- package/dist/capture/forked/forked-capture.d.mts +40 -0
- package/dist/capture/forked/forked-capture.mjs +240 -0
- package/dist/capture/forked/forked-capture.mjs.map +1 -0
- package/dist/capture/forked/lease-book.mjs +46 -0
- package/dist/capture/forked/lease-book.mjs.map +1 -0
- package/dist/capture/forked/protocol.d.mts +38 -0
- package/dist/capture/forked/protocol.mjs +25 -0
- package/dist/capture/forked/protocol.mjs.map +1 -0
- package/dist/capture/forked/runs.mjs +75 -0
- package/dist/capture/forked/runs.mjs.map +1 -0
- package/dist/capture/isolate.mjs +141 -0
- package/dist/capture/isolate.mjs.map +1 -0
- package/dist/capture/job-lease.d.mts +28 -0
- package/dist/capture/job-lease.mjs +57 -0
- package/dist/capture/job-lease.mjs.map +1 -0
- package/dist/capture/matrix.mjs +298 -0
- package/dist/capture/matrix.mjs.map +1 -0
- package/dist/capture/modes.d.mts +25 -0
- package/dist/capture/modes.mjs +107 -0
- package/dist/capture/modes.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +37 -5
- package/dist/capture/orchestrator.mjs +118 -107
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +9 -2
- package/dist/capture/pipeline.mjs +250 -45
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/progress.d.mts +11 -0
- package/dist/capture/progress.mjs +28 -0
- package/dist/capture/progress.mjs.map +1 -0
- package/dist/capture/publish/accept.d.mts +39 -0
- package/dist/capture/publish/accept.mjs +28 -0
- package/dist/capture/publish/accept.mjs.map +1 -0
- package/dist/capture/publish/complete.d.mts +46 -0
- package/dist/capture/publish/complete.mjs +64 -0
- package/dist/capture/publish/complete.mjs.map +1 -0
- package/dist/capture/publish/extract.d.mts +20 -0
- package/dist/capture/publish/extract.mjs +119 -0
- package/dist/capture/publish/extract.mjs.map +1 -0
- package/dist/capture/publish/handler.d.mts +28 -0
- package/dist/capture/publish/handler.mjs +62 -0
- package/dist/capture/publish/handler.mjs.map +1 -0
- package/dist/capture/publish/index.d.mts +10 -0
- package/dist/capture/publish/limits.d.mts +37 -0
- package/dist/capture/publish/limits.mjs +39 -0
- package/dist/capture/publish/limits.mjs.map +1 -0
- package/dist/capture/publish/published.d.mts +15 -0
- package/dist/capture/publish/published.mjs +18 -0
- package/dist/capture/publish/published.mjs.map +1 -0
- package/dist/capture/publish/run.d.mts +33 -0
- package/dist/capture/publish/run.mjs +30 -0
- package/dist/capture/publish/run.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +1 -1
- package/dist/capture/rediff.mjs +4 -3
- package/dist/capture/rediff.mjs.map +1 -1
- package/dist/capture/render.mjs +53 -0
- package/dist/capture/render.mjs.map +1 -0
- package/dist/capture/scratch.d.mts +44 -0
- package/dist/capture/scratch.mjs +138 -0
- package/dist/capture/scratch.mjs.map +1 -0
- package/dist/capture/shard-plan.mjs +128 -0
- package/dist/capture/shard-plan.mjs.map +1 -0
- package/dist/capture/shards/context.mjs +51 -0
- package/dist/capture/shards/context.mjs.map +1 -0
- package/dist/capture/shards/execute.mjs +174 -0
- package/dist/capture/shards/execute.mjs.map +1 -0
- package/dist/capture/shards/fan-out.mjs +126 -0
- package/dist/capture/shards/fan-out.mjs.map +1 -0
- package/dist/capture/shards/history.mjs +35 -0
- package/dist/capture/shards/history.mjs.map +1 -0
- package/dist/capture/shards/reduce.mjs +97 -0
- package/dist/capture/shards/reduce.mjs.map +1 -0
- package/dist/capture/sizing.mjs +1 -11
- package/dist/capture/sizing.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +2 -65
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +7 -8
- package/dist/capture/statics.mjs +25 -71
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/storybook-urls.d.mts +40 -0
- package/dist/capture/storybook-urls.mjs +41 -0
- package/dist/capture/storybook-urls.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +12 -4
- package/dist/capture/storybook.mjs +56 -16
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture/target.mjs +21 -0
- package/dist/capture/target.mjs.map +1 -0
- package/dist/capture/targets.d.mts +25 -0
- package/dist/capture/targets.mjs +32 -0
- package/dist/capture/targets.mjs.map +1 -0
- package/dist/capture/tests.d.mts +37 -0
- package/dist/capture/tests.mjs +126 -0
- package/dist/capture/tests.mjs.map +1 -0
- package/dist/capture/token-chunks.mjs +74 -0
- package/dist/capture/token-chunks.mjs.map +1 -0
- package/dist/capture.d.mts +60 -7
- package/dist/capture.mjs +48 -3
- package/dist/config.d.mts +121 -12
- package/dist/config.mjs +9 -6
- package/dist/config.mjs.map +1 -1
- package/dist/diff.d.mts +9 -3
- package/dist/diff.mjs +16 -3
- package/dist/diff.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/insights/budget.d.mts +20 -0
- package/dist/insights/budget.mjs +33 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/envelope.d.mts +59 -0
- package/dist/insights/envelope.mjs +60 -0
- package/dist/insights/envelope.mjs.map +1 -0
- package/dist/insights/evidence-health.d.mts +32 -0
- package/dist/insights/evidence-health.mjs +28 -0
- package/dist/insights/evidence-health.mjs.map +1 -0
- package/dist/insights/evidence.d.mts +55 -0
- package/dist/insights/evidence.mjs +64 -0
- package/dist/insights/evidence.mjs.map +1 -0
- package/dist/insights/hash.d.mts +18 -0
- package/dist/insights/hash.mjs +21 -0
- package/dist/insights/hash.mjs.map +1 -0
- package/dist/insights/limits.d.mts +24 -0
- package/dist/insights/limits.mjs +24 -0
- package/dist/insights/limits.mjs.map +1 -0
- package/dist/insights/prompts/v1.d.mts +13 -0
- package/dist/insights/prompts/v1.mjs +34 -0
- package/dist/insights/prompts/v1.mjs.map +1 -0
- package/dist/insights/redact.d.mts +9 -0
- package/dist/insights/redact.mjs +24 -0
- package/dist/insights/redact.mjs.map +1 -0
- package/dist/insights/text.mjs +44 -0
- package/dist/insights/text.mjs.map +1 -0
- package/dist/insights.d.mts +11 -0
- package/dist/insights.mjs +12 -0
- package/dist/logger.mjs +17 -1
- package/dist/logger.mjs.map +1 -1
- package/dist/models/ai-budget-alert.d.mts +23 -0
- package/dist/models/ai-budget-alert.mjs +38 -0
- package/dist/models/ai-budget-alert.mjs.map +1 -0
- package/dist/models/ai-usage.d.mts +40 -0
- package/dist/models/ai-usage.mjs +61 -0
- package/dist/models/ai-usage.mjs.map +1 -0
- package/dist/models/baseline-archive.d.mts +84 -0
- package/dist/models/baseline-archive.mjs +106 -0
- package/dist/models/baseline-archive.mjs.map +1 -0
- package/dist/models/baseline-export.d.mts +19 -0
- package/dist/models/baseline-export.mjs +136 -0
- package/dist/models/baseline-export.mjs.map +1 -0
- package/dist/models/baseline-import.mjs +118 -0
- package/dist/models/baseline-import.mjs.map +1 -0
- package/dist/models/baseline-transfer.d.mts +49 -0
- package/dist/models/baseline-transfer.mjs +121 -0
- package/dist/models/baseline-transfer.mjs.map +1 -0
- package/dist/models/baseline.d.mts +32 -5
- package/dist/models/baseline.mjs +65 -20
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build-component.d.mts +28 -0
- package/dist/models/build-component.mjs +55 -0
- package/dist/models/build-component.mjs.map +1 -0
- package/dist/models/build-review-status.d.mts +29 -0
- package/dist/models/build-review-status.mjs +37 -0
- package/dist/models/build-review-status.mjs.map +1 -0
- package/dist/models/build-review.d.mts +28 -0
- package/dist/models/build-review.mjs +54 -0
- package/dist/models/build-review.mjs.map +1 -0
- package/dist/models/build.d.mts +25 -3
- package/dist/models/build.mjs +50 -11
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +7 -1
- package/dist/models/capture-attempt.mjs +29 -4
- package/dist/models/capture-attempt.mjs.map +1 -1
- package/dist/models/capture-job.d.mts +106 -0
- package/dist/models/capture-job.mjs +288 -0
- package/dist/models/capture-job.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +1 -1
- package/dist/models/capture-log.mjs +4 -3
- package/dist/models/capture-log.mjs.map +1 -1
- package/dist/models/capture-shard.d.mts +62 -0
- package/dist/models/capture-shard.mjs +190 -0
- package/dist/models/capture-shard.mjs.map +1 -0
- package/dist/models/changeset.d.mts +48 -0
- package/dist/models/changeset.mjs +81 -0
- package/dist/models/changeset.mjs.map +1 -0
- package/dist/models/columns.mjs +19 -0
- package/dist/models/columns.mjs.map +1 -0
- package/dist/models/comment.d.mts +4 -1
- package/dist/models/comment.mjs +7 -4
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/component.d.mts +43 -0
- package/dist/models/component.mjs +60 -0
- package/dist/models/component.mjs.map +1 -0
- package/dist/models/connections.d.mts +41 -0
- package/dist/models/connections.mjs +112 -0
- package/dist/models/connections.mjs.map +1 -0
- package/dist/models/content-ref.d.mts +1 -1
- package/dist/models/content-ref.mjs +1 -1
- package/dist/models/content-ref.mjs.map +1 -1
- package/dist/models/credentials.mjs +12 -2
- package/dist/models/credentials.mjs.map +1 -1
- package/dist/models/design-credentials.d.mts +41 -0
- package/dist/models/design-credentials.mjs +86 -0
- package/dist/models/design-credentials.mjs.map +1 -0
- package/dist/models/design-health.d.mts +16 -0
- package/dist/models/design-health.mjs +25 -0
- package/dist/models/design-health.mjs.map +1 -0
- package/dist/models/design-nodes.d.mts +83 -0
- package/dist/models/design-nodes.mjs +222 -0
- package/dist/models/design-nodes.mjs.map +1 -0
- package/dist/models/design-renders.mjs +74 -0
- package/dist/models/design-renders.mjs.map +1 -0
- package/dist/models/design-resolve.d.mts +29 -0
- package/dist/models/design-resolve.mjs +41 -0
- package/dist/models/design-resolve.mjs.map +1 -0
- package/dist/models/design.d.mts +39 -0
- package/dist/models/design.mjs +90 -0
- package/dist/models/design.mjs.map +1 -0
- package/dist/models/inherited-ratio.d.mts +21 -0
- package/dist/models/inherited-ratio.mjs +22 -0
- package/dist/models/inherited-ratio.mjs.map +1 -0
- package/dist/models/insight.d.mts +70 -0
- package/dist/models/insight.mjs +162 -0
- package/dist/models/insight.mjs.map +1 -0
- package/dist/models/label.d.mts +4 -2
- package/dist/models/label.mjs +25 -10
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +1 -1
- package/dist/models/member.mjs +5 -4
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +37 -3
- package/dist/models/notification-channel.mjs +88 -9
- package/dist/models/notification-channel.mjs.map +1 -1
- package/dist/models/notification-subscription.d.mts +1 -2
- package/dist/models/notification-subscription.mjs +7 -9
- package/dist/models/notification-subscription.mjs.map +1 -1
- package/dist/models/project-group-mapping.d.mts +6 -1
- package/dist/models/project-group-mapping.mjs +14 -4
- package/dist/models/project-group-mapping.mjs.map +1 -1
- package/dist/models/project.d.mts +9 -1
- package/dist/models/project.mjs +9 -4
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/public-origin.d.mts +63 -0
- package/dist/models/public-origin.mjs +114 -0
- package/dist/models/public-origin.mjs.map +1 -0
- package/dist/models/review-switch-off.d.mts +14 -0
- package/dist/models/review-switch-off.mjs +26 -0
- package/dist/models/review-switch-off.mjs.map +1 -0
- package/dist/models/site-settings.d.mts +23 -0
- package/dist/models/site-settings.mjs +48 -0
- package/dist/models/site-settings.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +36 -1
- package/dist/models/snapshot.mjs +50 -3
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/token.d.mts +13 -1
- package/dist/models/token.mjs +23 -5
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/usage.d.mts +76 -0
- package/dist/models/usage.mjs +149 -0
- package/dist/models/usage.mjs.map +1 -0
- package/dist/models/user.d.mts +1 -1
- package/dist/models/user.mjs +4 -3
- package/dist/models/user.mjs.map +1 -1
- package/dist/models/webhook.d.mts +15 -1
- package/dist/models/webhook.mjs +33 -4
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +26 -3
- package/dist/models.mjs +27 -5
- package/dist/options.d.mts +18 -0
- package/dist/options.mjs +90 -0
- package/dist/options.mjs.map +1 -0
- package/dist/paths.d.mts +19 -4
- package/dist/paths.mjs +51 -10
- package/dist/paths.mjs.map +1 -1
- package/dist/retention/ai.d.mts +18 -0
- package/dist/retention/ai.mjs +23 -0
- package/dist/retention/ai.mjs.map +1 -0
- package/dist/retention/pending.mjs +47 -0
- package/dist/retention/pending.mjs.map +1 -0
- package/dist/retention/screenshot-sweep.d.mts +51 -0
- package/dist/retention/screenshot-sweep.mjs +128 -0
- package/dist/retention/screenshot-sweep.mjs.map +1 -0
- package/dist/retention.d.mts +16 -2
- package/dist/retention.mjs +71 -14
- package/dist/retention.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +6 -0
- package/dist/schema/build-component.d.mts +25 -0
- package/dist/schema/build-review.d.mts +17 -0
- package/dist/schema/build.d.mts +7 -0
- package/dist/schema/capture-job.d.mts +60 -0
- package/dist/schema/capture-shard.d.mts +35 -0
- package/dist/schema/comment.d.mts +2 -0
- package/dist/schema/connection.d.mts +24 -0
- package/dist/schema/design.d.mts +58 -0
- package/dist/schema/insight.d.mts +54 -0
- package/dist/schema/notification.d.mts +4 -0
- package/dist/schema/project.d.mts +21 -0
- package/dist/schema/site-setting.d.mts +21 -0
- package/dist/schema/snapshot.d.mts +12 -0
- package/dist/schema/usage.d.mts +30 -0
- package/dist/schema.d.mts +11 -3
- package/dist/serverless.d.mts +21 -0
- package/dist/serverless.mjs +37 -0
- package/dist/serverless.mjs.map +1 -0
- package/dist/test-helpers/create-project.d.mts +1 -1
- package/dist/test-helpers/deferred.d.mts +39 -0
- package/dist/test-helpers/deferred.mjs +61 -0
- package/dist/test-helpers/deferred.mjs.map +1 -0
- package/dist/test-helpers/design-contract.d.mts +21 -0
- package/dist/test-helpers/design-contract.mjs +40 -0
- package/dist/test-helpers/design-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-database.mjs +14 -2
- package/dist/test-helpers/fake-database.mjs.map +1 -1
- package/dist/test-helpers/fake-design.d.mts +43 -0
- package/dist/test-helpers/fake-design.mjs +114 -0
- package/dist/test-helpers/fake-design.mjs.map +1 -0
- package/dist/test-helpers/fake-notifier.d.mts +21 -0
- package/dist/test-helpers/fake-notifier.mjs +44 -0
- package/dist/test-helpers/fake-notifier.mjs.map +1 -0
- package/dist/test-helpers/fake-tables.mjs +255 -67
- package/dist/test-helpers/fake-tables.mjs.map +1 -1
- package/dist/test-helpers/manual-clock.d.mts +27 -0
- package/dist/test-helpers/manual-clock.mjs +56 -0
- package/dist/test-helpers/manual-clock.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +7 -1
- package/dist/test-helpers/notifier-contract.mjs +32 -3
- package/dist/test-helpers/notifier-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-contract.d.mts +16 -2
- package/dist/test-helpers/queue-contract.mjs +195 -1
- package/dist/test-helpers/queue-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
- package/dist/test-helpers/queue-multi-instance.mjs +206 -0
- package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.mjs +11 -0
- package/dist/test-helpers/runner-contract.mjs.map +1 -1
- package/dist/test-helpers/sql-chunks.mjs +23 -0
- package/dist/test-helpers/sql-chunks.mjs.map +1 -1
- package/dist/test-helpers/storage-contract.mjs +1 -1
- package/dist/test-helpers.d.mts +9 -4
- package/dist/test-helpers.mjs +8 -3
- package/dist/types.d.mts +14 -3
- package/dist/types.mjs +21 -4
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +4 -0
- package/dist/urls.mjs +11 -2
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/adaptive-concurrency.d.mts +66 -0
- package/dist/utils/adaptive-concurrency.mjs +149 -0
- package/dist/utils/adaptive-concurrency.mjs.map +1 -0
- package/dist/utils/connect-retry.d.mts +26 -0
- package/dist/utils/connect-retry.mjs +43 -0
- package/dist/utils/connect-retry.mjs.map +1 -0
- package/dist/utils/environment.d.mts +19 -0
- package/dist/utils/environment.mjs +59 -0
- package/dist/utils/environment.mjs.map +1 -0
- package/dist/utils/http.d.mts +17 -1
- package/dist/utils/http.mjs +22 -3
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/map-adaptive.d.mts +15 -0
- package/dist/utils/map-adaptive.mjs +72 -0
- package/dist/utils/map-adaptive.mjs.map +1 -0
- package/dist/utils/map-limit.d.mts +10 -0
- package/dist/utils/map-limit.mjs +32 -0
- package/dist/utils/map-limit.mjs.map +1 -0
- package/dist/utils/page-limiter.d.mts +37 -0
- package/dist/utils/page-limiter.mjs +56 -0
- package/dist/utils/page-limiter.mjs.map +1 -0
- package/dist/utils/public-origin.d.mts +20 -0
- package/dist/utils/public-origin.mjs +75 -0
- package/dist/utils/public-origin.mjs.map +1 -0
- package/dist/utils/redact.mjs +3 -0
- package/dist/utils/redact.mjs.map +1 -1
- package/dist/utils/remote-environment.d.mts +39 -0
- package/dist/utils/remote-environment.mjs +114 -0
- package/dist/utils/remote-environment.mjs.map +1 -0
- package/dist/utils/resources.d.mts +64 -0
- package/dist/utils/resources.mjs +122 -0
- package/dist/utils/resources.mjs.map +1 -0
- package/dist/utils/screenshot-store.d.mts +15 -0
- package/dist/utils/screenshot-store.mjs +41 -0
- package/dist/utils/screenshot-store.mjs.map +1 -0
- package/dist/utils/session-gate.d.mts +22 -0
- package/dist/utils/session-gate.mjs +53 -0
- package/dist/utils/session-gate.mjs.map +1 -0
- package/dist/utils/storybook-token.d.mts +81 -0
- package/dist/utils/storybook-token.mjs +134 -0
- package/dist/utils/storybook-token.mjs.map +1 -0
- package/dist/utils/storybook-url.d.mts +15 -0
- package/dist/utils/storybook-url.mjs +21 -0
- package/dist/utils/storybook-url.mjs.map +1 -0
- package/dist/utils/timing.mjs +1 -1
- package/dist/utils/timing.mjs.map +1 -1
- package/dist/utils/zip-read.mjs +129 -0
- package/dist/utils/zip-read.mjs.map +1 -0
- package/dist/utils/zip-write.d.mts +18 -0
- package/dist/utils/zip-write.mjs +188 -0
- package/dist/utils/zip-write.mjs.map +1 -0
- package/dist/utils.d.mts +17 -3
- package/dist/utils.mjs +18 -4
- package/package.json +32 -20
- package/dist/adapter/capture-runner.mjs.map +0 -1
- package/dist/adapter/git-host/comments.d.mts +0 -41
- package/dist/adapter/git-host/comments.mjs +0 -54
- package/dist/adapter/git-host/comments.mjs.map +0 -1
- package/dist/adapter/git-host/helpers.d.mts +0 -13
- package/dist/adapter/git-host/helpers.mjs +0 -30
- package/dist/adapter/git-host/helpers.mjs.map +0 -1
- package/dist/adapter/git-host.d.mts +0 -42
- package/dist/capture/queue.d.mts +0 -38
- package/dist/capture/queue.mjs +0 -98
- package/dist/capture/queue.mjs.map +0 -1
- package/dist/capture/recovery.d.mts +0 -21
- package/dist/capture/recovery.mjs +0 -83
- package/dist/capture/recovery.mjs.map +0 -1
- package/dist/capture/status-fanout.mjs +0 -72
- package/dist/capture/status-fanout.mjs.map +0 -1
- package/dist/diff/engine.d.mts +0 -16
- package/dist/diff/engine.mjs +0 -87
- package/dist/diff/engine.mjs.map +0 -1
- package/dist/diff/options.d.mts +0 -42
- package/dist/diff/options.mjs +0 -15
- package/dist/diff/options.mjs.map +0 -1
- package/dist/models/status-config.d.mts +0 -38
- package/dist/models/status-config.mjs +0 -63
- package/dist/models/status-config.mjs.map +0 -1
- package/dist/schema/status-config.d.mts +0 -16
- package/dist/test-helpers/git-contract.d.mts +0 -11
- package/dist/test-helpers/git-contract.mjs +0 -34
- package/dist/test-helpers/git-contract.mjs.map +0 -1
- /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { classifyFailures } from "../failure-sets.mjs";
|
|
5
|
+
import { renderWork } from "../matrix.mjs";
|
|
6
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
7
|
+
import { persistRendered } from "../pipeline.mjs";
|
|
8
|
+
import { removeShardScratch } from "../scratch.mjs";
|
|
9
|
+
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
10
|
+
import { contextOf, matrixInputOf } from "./context.mjs";
|
|
11
|
+
import { extractStorybookToScratch } from "../statics.mjs";
|
|
12
|
+
import { loadTarget } from "../target.mjs";
|
|
13
|
+
import { reduceBuild } from "./reduce.mjs";
|
|
14
|
+
//#region src/capture/shards/execute.ts
|
|
15
|
+
/** The render stage: one shard of a sharded capture, run by whichever worker picked up its job. */
|
|
16
|
+
/**
|
|
17
|
+
* Render and persist one shard, record its outcome, and, if it was the last one, finalize the build.
|
|
18
|
+
*
|
|
19
|
+
* Safe to run twice: a shard already done is not rendered again, and a rerun of an interrupted one
|
|
20
|
+
* starts from a clean slate for its stories. A redelivered shard that is already done still tries
|
|
21
|
+
* to finalize the build, in case the run that finished it died or failed before it could.
|
|
22
|
+
*/
|
|
23
|
+
async function executeCaptureShard(input, options) {
|
|
24
|
+
const picked = await pickShard(input, options);
|
|
25
|
+
if ("status" in picked) return picked;
|
|
26
|
+
const final = input.final !== false;
|
|
27
|
+
if (picked.shard.status === "done") return await reduceFinished(picked.target, input, final);
|
|
28
|
+
await runShard(picked.target, picked.shard, input, final);
|
|
29
|
+
return await reduceRendered(picked.target, input, final);
|
|
30
|
+
}
|
|
31
|
+
/** The shard is done: if it was the last one, this run finalizes the build. */
|
|
32
|
+
async function reduceRendered(target, input, final) {
|
|
33
|
+
const summary = await reduceBuild(target, final);
|
|
34
|
+
if (!summary) return {
|
|
35
|
+
status: "rendered",
|
|
36
|
+
finalized: false
|
|
37
|
+
};
|
|
38
|
+
await input.onFinalized?.(summary);
|
|
39
|
+
return {
|
|
40
|
+
status: "rendered",
|
|
41
|
+
finalized: true,
|
|
42
|
+
summary
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A redelivered shard that an earlier run finished: finalize the build if nobody has. */
|
|
46
|
+
async function reduceFinished(target, input, final) {
|
|
47
|
+
const summary = await reduceBuild(target, final);
|
|
48
|
+
if (!summary) return {
|
|
49
|
+
status: "skipped",
|
|
50
|
+
reason: "shard already finished"
|
|
51
|
+
};
|
|
52
|
+
await input.onFinalized?.(summary);
|
|
53
|
+
return {
|
|
54
|
+
status: "reduced",
|
|
55
|
+
summary
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Run the shard; a failure is recorded on its row (and, if final, reduces the build) then rethrown. */
|
|
59
|
+
async function runShard(target, shard, input, final) {
|
|
60
|
+
const shards = new CaptureShardModel(target.options.db);
|
|
61
|
+
await shards.markRunning(shard.id);
|
|
62
|
+
input.progress?.();
|
|
63
|
+
try {
|
|
64
|
+
await renderShard(target, shard, input.progress);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await recordFailure(shards, shard, error, final);
|
|
67
|
+
if (final) await finalizeAfterFailure(target, input, error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The shard failed for good: if it was the last one open, it ends the build, and only then does
|
|
73
|
+
* this run report it. A failure here is logged, not thrown: the shard's own error is the news.
|
|
74
|
+
*/
|
|
75
|
+
async function finalizeAfterFailure(target, input, error) {
|
|
76
|
+
try {
|
|
77
|
+
const summary = await reduceBuild(target, true);
|
|
78
|
+
if (summary) await input.onFinalized?.(summary, error);
|
|
79
|
+
} catch (reduceError) {
|
|
80
|
+
target.options.logger?.error({
|
|
81
|
+
err: reduceError,
|
|
82
|
+
buildId: target.build.id
|
|
83
|
+
}, "finalizing after a failed shard failed");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The shard to run and what to run it against, or why there is nothing to do. */
|
|
87
|
+
async function pickShard(input, options) {
|
|
88
|
+
const shard = await new CaptureShardModel(options.db).get(input.shard);
|
|
89
|
+
if (shard?.kind !== "render") return {
|
|
90
|
+
status: "skipped",
|
|
91
|
+
reason: "shard no longer exists"
|
|
92
|
+
};
|
|
93
|
+
const { build, project } = await loadTarget(options, input.buildId);
|
|
94
|
+
if (build.status !== "capturing") return {
|
|
95
|
+
status: "skipped",
|
|
96
|
+
reason: shard.status === "done" ? "shard already finished" : `build is ${build.status}`
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
target: {
|
|
100
|
+
options,
|
|
101
|
+
build,
|
|
102
|
+
project
|
|
103
|
+
},
|
|
104
|
+
shard
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function recordFailure(shards, shard, error, final) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
await (final ? shards.fail(shard.id, message) : shards.retry(shard.id, message));
|
|
110
|
+
}
|
|
111
|
+
async function renderShard(target, shard, progress) {
|
|
112
|
+
const { options, build, project } = target;
|
|
113
|
+
const payload = JSON.parse(shard.payload ?? "{}");
|
|
114
|
+
const started = performance.now();
|
|
115
|
+
const storybookDir = await extractStorybookToScratch(options.storage, options.scratchDir, project.id, build.id, shard.id, options.limits);
|
|
116
|
+
progress?.();
|
|
117
|
+
try {
|
|
118
|
+
const result = await renderAndPersist(target, payload, {
|
|
119
|
+
storybookDir,
|
|
120
|
+
runId: shard.id,
|
|
121
|
+
progress
|
|
122
|
+
});
|
|
123
|
+
const durationMs = Math.round(performance.now() - started);
|
|
124
|
+
await new CaptureShardModel(options.db).complete(shard.id, JSON.stringify({
|
|
125
|
+
...result,
|
|
126
|
+
durationMs
|
|
127
|
+
}));
|
|
128
|
+
} finally {
|
|
129
|
+
await removeShardScratch(options.scratchDir, project.id, build.id, shard.id, options.logger);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function renderAndPersist(target, payload, run) {
|
|
133
|
+
const stories = payload.browsers.flatMap((engine) => engine.stories);
|
|
134
|
+
const browsers = payload.browsers.map((engine) => engine.browser);
|
|
135
|
+
const rendered = await renderWork(matrixInputOf(target, {
|
|
136
|
+
stories,
|
|
137
|
+
viewports: payload.viewports,
|
|
138
|
+
browsers,
|
|
139
|
+
storybookDir: run.storybookDir,
|
|
140
|
+
affectedPaths: null,
|
|
141
|
+
progress: run.progress,
|
|
142
|
+
runId: run.runId
|
|
143
|
+
}), payload.browsers);
|
|
144
|
+
const failures = classifyFailures(stories, rendered.failures);
|
|
145
|
+
await clearEarlierRun(target, payload);
|
|
146
|
+
const persisted = await persistRendered(contextOf(target, {
|
|
147
|
+
viewports: payload.viewports,
|
|
148
|
+
browsers,
|
|
149
|
+
captures: rendered.captures,
|
|
150
|
+
freshBrowsers: new Set(payload.fresh),
|
|
151
|
+
progress: run.progress
|
|
152
|
+
}), failures.blocking, failures.flaky, failures.a11y);
|
|
153
|
+
return {
|
|
154
|
+
failedStoryIds: [...persisted.failed],
|
|
155
|
+
flakyStoryIds: [...persisted.flaky],
|
|
156
|
+
a11yStoryIds: [...persisted.a11y],
|
|
157
|
+
captured: rendered.captures.length
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Drop what an interrupted earlier run of this shard persisted: the rendered rows of the
|
|
162
|
+
* (story, engine) pairs it is about to write. The plan's inherited rows stay, including those of
|
|
163
|
+
* these stories on engines or viewports this shard does not render.
|
|
164
|
+
*/
|
|
165
|
+
async function clearEarlierRun(target, payload) {
|
|
166
|
+
await new SnapshotModel(target.options.db, target.options.tables).removeRendered(target.build.id, payload.browsers.map((engine) => ({
|
|
167
|
+
browser: engine.browser,
|
|
168
|
+
storyIds: new Set(engine.stories.map((story) => story.id))
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { executeCaptureShard };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=execute.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.mjs","names":[],"sources":["../../../src/capture/shards/execute.ts"],"sourcesContent":["/** The render stage: one shard of a sharded capture, run by whichever worker picked up its job. */\nimport { CaptureShardModel } from \"../../models/capture-shard.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { CaptureShard } from \"../../schema/capture-shard.ts\";\nimport { classifyFailures } from \"../failure-sets.ts\";\nimport { renderWork } from \"../matrix.ts\";\nimport type { CaptureJobOptions } from \"../orchestrator.ts\";\nimport { persistRendered } from \"../pipeline.ts\";\nimport type { CaptureProgress } from \"../progress.ts\";\nimport { removeShardScratch } from \"../scratch.ts\";\nimport type { ShardPayload, ShardResult } from \"../shard-plan.ts\";\nimport { extractStorybookToScratch } from \"../statics.ts\";\nimport { loadTarget } from \"../target.ts\";\nimport { contextOf, matrixInputOf, type ShardTarget } from \"./context.ts\";\nimport { reduceBuild, type FinalSummary } from \"./reduce.ts\";\n\n/** A shard job being run. */\nexport interface ShardRunInput {\n buildId: string;\n shard: string;\n reqId?: string | undefined;\n /**\n * Whether a failure now is the last word. A queue that redelivers failed jobs passes false until\n * its retries are used up, so a shard that will be retried does not end the build.\n */\n final?: boolean | undefined;\n /** Reports render and persist progress to the job's lease (see `leaseProgress`). */\n progress?: CaptureProgress | undefined;\n /**\n * Called once by the run that finalized the build, and by no other, with the summary and, when\n * the shard that ended the build failed, its error. The caller closes the plan's attempt here.\n */\n onFinalized?: ((summary: FinalSummary, error?: unknown) => Promise<void>) | undefined;\n}\n\n/** How a shard run ended. */\nexport type ShardOutcome =\n | { status: \"skipped\"; reason: string }\n | { status: \"rendered\"; finalized: false }\n | { status: \"rendered\"; finalized: true; summary: FinalSummary }\n /** The shard was already done, but the build was not: this run finalized it. */\n | { status: \"reduced\"; summary: FinalSummary };\n\n/**\n * Render and persist one shard, record its outcome, and, if it was the last one, finalize the build.\n *\n * Safe to run twice: a shard already done is not rendered again, and a rerun of an interrupted one\n * starts from a clean slate for its stories. A redelivered shard that is already done still tries\n * to finalize the build, in case the run that finished it died or failed before it could.\n */\nexport async function executeCaptureShard(\n input: ShardRunInput,\n options: CaptureJobOptions,\n): Promise<ShardOutcome> {\n const picked = await pickShard(input, options);\n if (\"status\" in picked) {\n return picked;\n }\n const final = input.final !== false;\n if (picked.shard.status === \"done\") {\n return await reduceFinished(picked.target, input, final);\n }\n await runShard(picked.target, picked.shard, input, final);\n return await reduceRendered(picked.target, input, final);\n}\n\n/** The shard is done: if it was the last one, this run finalizes the build. */\nasync function reduceRendered(\n target: ShardTarget,\n input: ShardRunInput,\n final: boolean,\n): Promise<ShardOutcome> {\n const summary = await reduceBuild(target, final);\n if (!summary) {\n return { status: \"rendered\", finalized: false };\n }\n await input.onFinalized?.(summary);\n return { status: \"rendered\", finalized: true, summary };\n}\n\n/** A redelivered shard that an earlier run finished: finalize the build if nobody has. */\nasync function reduceFinished(\n target: ShardTarget,\n input: ShardRunInput,\n final: boolean,\n): Promise<ShardOutcome> {\n const summary = await reduceBuild(target, final);\n if (!summary) {\n return { status: \"skipped\", reason: \"shard already finished\" };\n }\n await input.onFinalized?.(summary);\n return { status: \"reduced\", summary };\n}\n\n/** Run the shard; a failure is recorded on its row (and, if final, reduces the build) then rethrown. */\nasync function runShard(\n target: ShardTarget,\n shard: CaptureShard,\n input: ShardRunInput,\n final: boolean,\n): Promise<void> {\n const shards = new CaptureShardModel(target.options.db);\n await shards.markRunning(shard.id);\n input.progress?.();\n try {\n await renderShard(target, shard, input.progress);\n } catch (error) {\n await recordFailure(shards, shard, error, final);\n if (final) {\n await finalizeAfterFailure(target, input, error);\n }\n throw error;\n }\n}\n\n/**\n * The shard failed for good: if it was the last one open, it ends the build, and only then does\n * this run report it. A failure here is logged, not thrown: the shard's own error is the news.\n */\nasync function finalizeAfterFailure(\n target: ShardTarget,\n input: ShardRunInput,\n error: unknown,\n): Promise<void> {\n try {\n const summary = await reduceBuild(target, true);\n if (summary) {\n await input.onFinalized?.(summary, error);\n }\n } catch (reduceError) {\n target.options.logger?.error(\n { err: reduceError, buildId: target.build.id },\n \"finalizing after a failed shard failed\",\n );\n }\n}\n\n/** The shard to run and what to run it against, or why there is nothing to do. */\nasync function pickShard(\n input: ShardRunInput,\n options: CaptureJobOptions,\n): Promise<\n { target: ShardTarget; shard: CaptureShard } | Extract<ShardOutcome, { status: \"skipped\" }>\n> {\n const shard = await new CaptureShardModel(options.db).get(input.shard);\n if (shard?.kind !== \"render\") {\n return { status: \"skipped\", reason: \"shard no longer exists\" };\n }\n const { build, project } = await loadTarget(options, input.buildId);\n if (build.status !== \"capturing\") {\n const reason = shard.status === \"done\" ? \"shard already finished\" : `build is ${build.status}`;\n return { status: \"skipped\", reason };\n }\n return { target: { options, build, project }, shard };\n}\n\nasync function recordFailure(\n shards: CaptureShardModel,\n shard: CaptureShard,\n error: unknown,\n final: boolean,\n): Promise<void> {\n const message = error instanceof Error ? error.message : String(error);\n await (final ? shards.fail(shard.id, message) : shards.retry(shard.id, message));\n}\n\nasync function renderShard(\n target: ShardTarget,\n shard: CaptureShard,\n progress: CaptureProgress | undefined,\n): Promise<void> {\n const { options, build, project } = target;\n const payload = JSON.parse(shard.payload ?? \"{}\") as ShardPayload;\n const started = performance.now();\n const storybookDir = await extractStorybookToScratch(\n options.storage,\n options.scratchDir,\n project.id,\n build.id,\n shard.id,\n options.limits,\n );\n progress?.();\n try {\n const result = await renderAndPersist(target, payload, {\n storybookDir,\n runId: shard.id,\n progress,\n });\n const durationMs = Math.round(performance.now() - started);\n await new CaptureShardModel(options.db).complete(\n shard.id,\n JSON.stringify({ ...result, durationMs } satisfies ShardResult),\n );\n } finally {\n await removeShardScratch(options.scratchDir, project.id, build.id, shard.id, options.logger);\n }\n}\n\nasync function renderAndPersist(\n target: ShardTarget,\n payload: ShardPayload,\n run: { storybookDir: string; runId: string; progress: CaptureProgress | undefined },\n): Promise<Omit<ShardResult, \"durationMs\">> {\n const stories = payload.browsers.flatMap((engine) => engine.stories);\n const browsers = payload.browsers.map((engine) => engine.browser);\n const rendered = await renderWork(\n matrixInputOf(target, {\n stories,\n viewports: payload.viewports,\n browsers,\n storybookDir: run.storybookDir,\n affectedPaths: null,\n progress: run.progress,\n runId: run.runId,\n }),\n payload.browsers,\n );\n const failures = classifyFailures(stories, rendered.failures);\n await clearEarlierRun(target, payload);\n const persisted = await persistRendered(\n contextOf(target, {\n viewports: payload.viewports,\n browsers,\n captures: rendered.captures,\n freshBrowsers: new Set(payload.fresh),\n progress: run.progress,\n }),\n failures.blocking,\n failures.flaky,\n failures.a11y,\n );\n return {\n failedStoryIds: [...persisted.failed],\n flakyStoryIds: [...persisted.flaky],\n a11yStoryIds: [...persisted.a11y],\n captured: rendered.captures.length,\n };\n}\n\n/**\n * Drop what an interrupted earlier run of this shard persisted: the rendered rows of the\n * (story, engine) pairs it is about to write. The plan's inherited rows stay, including those of\n * these stories on engines or viewports this shard does not render.\n */\nasync function clearEarlierRun(target: ShardTarget, payload: ShardPayload): Promise<void> {\n await new SnapshotModel(target.options.db, target.options.tables).removeRendered(\n target.build.id,\n payload.browsers.map((engine) => ({\n browser: engine.browser,\n storyIds: new Set(engine.stories.map((story) => story.id)),\n })),\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { CaptureAttemptModel, emitAttemptLog } from "../../models/capture-attempt.mjs";
|
|
5
|
+
import { resolveStoryViewports } from "../storybook.mjs";
|
|
6
|
+
import { affectedPathsFor } from "../affected.mjs";
|
|
7
|
+
import { partitionBrowsers } from "../matrix.mjs";
|
|
8
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
9
|
+
import { freshBrowsersOf, persistInherited } from "../pipeline.mjs";
|
|
10
|
+
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
11
|
+
import { planShards, worthSharding } from "../shard-plan.mjs";
|
|
12
|
+
import { contextOf, matrixInputOf } from "./context.mjs";
|
|
13
|
+
import { storyDurations } from "./history.mjs";
|
|
14
|
+
//#region src/capture/shards/fan-out.ts
|
|
15
|
+
/**
|
|
16
|
+
* The plan stage. When sharding is on and the build is big enough, decide what to render, record
|
|
17
|
+
* the plan and its shards, and hand the shards to the queue; otherwise return null and the caller
|
|
18
|
+
* renders the build in one piece as it always has.
|
|
19
|
+
*
|
|
20
|
+
* Inherited snapshots (affected capture) are written here, since nothing renders for them.
|
|
21
|
+
*/
|
|
22
|
+
async function fanOutCapture(args) {
|
|
23
|
+
const { options } = args;
|
|
24
|
+
if (!options.shardSize || !options.enqueue) return null;
|
|
25
|
+
const matrix = matrixInputOf(args, {
|
|
26
|
+
stories: args.stories,
|
|
27
|
+
viewports: args.viewports,
|
|
28
|
+
browsers: args.browsers,
|
|
29
|
+
storybookDir: "",
|
|
30
|
+
affectedPaths: affectedPathsFor(args.build),
|
|
31
|
+
logger: args.logger,
|
|
32
|
+
recordLog: args.input.recordLog,
|
|
33
|
+
progress: args.input.progress
|
|
34
|
+
});
|
|
35
|
+
const partition = await partitionBrowsers(matrix);
|
|
36
|
+
const capturesOf = (story) => resolveStoryViewports(story, args.viewports).length;
|
|
37
|
+
return worthSharding(partition.work, capturesOf, options.shardSize) ? await planAndCommit(args, partition, capturesOf) : null;
|
|
38
|
+
}
|
|
39
|
+
async function planAndCommit(args, partition, capturesOf) {
|
|
40
|
+
const durations = await storyDurations(args);
|
|
41
|
+
const slices = planShards(partition.work, capturesOf, args.options.shardSize, (storyId) => durations.get(storyId));
|
|
42
|
+
return slices.length === 0 ? null : await commitPlan({
|
|
43
|
+
...args,
|
|
44
|
+
timed: durations.size > 0
|
|
45
|
+
}, slices, partition);
|
|
46
|
+
}
|
|
47
|
+
async function commitPlan(args, slices, partition) {
|
|
48
|
+
await supersedeEarlierRuns(args);
|
|
49
|
+
const ctx = contextOf(args, {
|
|
50
|
+
viewports: args.viewports,
|
|
51
|
+
browsers: args.browsers,
|
|
52
|
+
inherited: partition.inherited,
|
|
53
|
+
progress: args.input.progress
|
|
54
|
+
});
|
|
55
|
+
const failedInherits = await persistInherited(ctx);
|
|
56
|
+
const storyIds = /* @__PURE__ */ new Set([...partition.work.flatMap((engine) => engine.stories.map((story) => story.id)), ...partition.inherited.map((inherited) => inherited.story.id)]);
|
|
57
|
+
const fresh = [...await freshBrowsersOf(ctx)];
|
|
58
|
+
const failed = /* @__PURE__ */ new Set([...failedInherits, ...args.testFailed ?? []]);
|
|
59
|
+
const rows = await recordPlan(args, slices, {
|
|
60
|
+
fresh,
|
|
61
|
+
storyIds,
|
|
62
|
+
failed
|
|
63
|
+
});
|
|
64
|
+
await handOut(args, rows);
|
|
65
|
+
await emitAttemptLog(args.input.recordLog, args.logger, "info", "capture planned", {
|
|
66
|
+
shards: rows.length,
|
|
67
|
+
stories: storyIds.size,
|
|
68
|
+
ordering: args.timed ? "longest-first" : "by-capture-count"
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
storyCount: storyIds.size,
|
|
72
|
+
failedCount: failed.size,
|
|
73
|
+
shards: rows.length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A new plan of a build (recovery requeue, retry, a redelivered plan job) starts clean: the
|
|
78
|
+
* snapshots an earlier plan or its shards wrote are dropped, so inherited rows are not doubled,
|
|
79
|
+
* and the attempts it left open are closed, since only the newest plan's attempt is ever closed.
|
|
80
|
+
*/
|
|
81
|
+
async function supersedeEarlierRuns(args) {
|
|
82
|
+
const { db, tables } = args.options;
|
|
83
|
+
await new SnapshotModel(db, tables).removeForBuild(args.build.id);
|
|
84
|
+
await new CaptureAttemptModel(db, tables).closeOthers(args.build.id, args.input.attempt?.id ?? null, "superseded by a new capture plan");
|
|
85
|
+
}
|
|
86
|
+
async function recordPlan(args, slices, plan) {
|
|
87
|
+
const shards = new CaptureShardModel(args.options.db);
|
|
88
|
+
await shards.clearBuild(args.build.id);
|
|
89
|
+
const settings = {
|
|
90
|
+
browsers: args.browsers,
|
|
91
|
+
fresh: plan.fresh,
|
|
92
|
+
shardSize: args.options.shardSize ?? 0,
|
|
93
|
+
total: slices.length
|
|
94
|
+
};
|
|
95
|
+
const result = {
|
|
96
|
+
storyIds: [...plan.storyIds],
|
|
97
|
+
failedStoryIds: [...plan.failed]
|
|
98
|
+
};
|
|
99
|
+
return await shards.createPlan(args.project.id, args.build.id, {
|
|
100
|
+
attemptId: args.input.attempt?.id ?? null,
|
|
101
|
+
payload: JSON.stringify(settings),
|
|
102
|
+
result: JSON.stringify(result)
|
|
103
|
+
}, slices.map((browsers) => ({ payload: shardPayload(args, browsers, plan.fresh) })));
|
|
104
|
+
}
|
|
105
|
+
function shardPayload(args, browsers, fresh) {
|
|
106
|
+
const payload = {
|
|
107
|
+
browsers,
|
|
108
|
+
viewports: args.viewports,
|
|
109
|
+
fresh
|
|
110
|
+
};
|
|
111
|
+
return JSON.stringify(payload);
|
|
112
|
+
}
|
|
113
|
+
/** Enqueue every shard in order; the caller marks the build failed if the queue refuses one. */
|
|
114
|
+
async function handOut(args, rows) {
|
|
115
|
+
const { enqueue } = args.options;
|
|
116
|
+
for (const row of rows) await enqueue?.({
|
|
117
|
+
buildId: args.build.id,
|
|
118
|
+
shard: row.id,
|
|
119
|
+
reqId: args.input.reqId,
|
|
120
|
+
traceparent: args.input.traceparent
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { fanOutCapture };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=fan-out.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fan-out.mjs","names":[],"sources":["../../../src/capture/shards/fan-out.ts"],"sourcesContent":["/** The plan stage: decide what to render, record it as shards, and hand the shards to the queue. */\nimport type { Logger } from \"pino\";\nimport type { CaptureTarget } from \"../../adapters/capture-runner.ts\";\nimport { CaptureAttemptModel, emitAttemptLog } from \"../../models/capture-attempt.ts\";\nimport { CaptureShardModel } from \"../../models/capture-shard.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { CaptureShard } from \"../../schema/capture-shard.ts\";\nimport type { Viewport } from \"../adapter.ts\";\nimport { affectedPathsFor, type InheritedStory } from \"../affected.ts\";\nimport { partitionBrowsers, type MatrixInput } from \"../matrix.ts\";\nimport type { CaptureJobInput, CaptureSummary } from \"../orchestrator.ts\";\nimport { freshBrowsersOf, persistInherited } from \"../pipeline.ts\";\nimport {\n planShards,\n worthSharding,\n type EngineWork,\n type PlanPayload,\n type PlanResult,\n type ShardPayload,\n} from \"../shard-plan.ts\";\nimport { resolveStoryViewports } from \"../storybook.ts\";\nimport { contextOf, matrixInputOf, type ShardTarget } from \"./context.ts\";\nimport { storyDurations } from \"./history.ts\";\n\n/** Inputs for {@link fanOutCapture}: the plan stage's view of the build. */\nexport interface FanOutInput extends ShardTarget {\n input: CaptureJobInput;\n stories: MatrixInput[\"stories\"];\n viewports: Viewport[];\n browsers: CaptureTarget[];\n logger: Logger | undefined;\n /** Test entries that failed in the plan stage (ADR 0004, decision 10): they fail the build. */\n testFailed?: ReadonlySet<string> | undefined;\n /** Set by the plan stage: whether past timings were available to order the shards by. */\n timed?: boolean;\n}\n\n/**\n * The plan stage. When sharding is on and the build is big enough, decide what to render, record\n * the plan and its shards, and hand the shards to the queue; otherwise return null and the caller\n * renders the build in one piece as it always has.\n *\n * Inherited snapshots (affected capture) are written here, since nothing renders for them.\n */\nexport async function fanOutCapture(args: FanOutInput): Promise<CaptureSummary | null> {\n const { options } = args;\n if (!options.shardSize || !options.enqueue) {\n return null;\n }\n const matrix = matrixInputOf(args, {\n stories: args.stories,\n viewports: args.viewports,\n browsers: args.browsers,\n storybookDir: \"\",\n affectedPaths: affectedPathsFor(args.build),\n logger: args.logger,\n recordLog: args.input.recordLog,\n progress: args.input.progress,\n });\n const partition = await partitionBrowsers(matrix);\n const capturesOf = (story: MatrixInput[\"stories\"][number]): number =>\n resolveStoryViewports(story, args.viewports).length;\n // Past timings only order the shards: not worth a lookup for a build that runs in one piece.\n return worthSharding(partition.work, capturesOf, options.shardSize)\n ? await planAndCommit(args, partition, capturesOf)\n : null;\n}\n\nasync function planAndCommit(\n args: FanOutInput,\n partition: { work: EngineWork[]; inherited: InheritedStory[] },\n capturesOf: (story: MatrixInput[\"stories\"][number]) => number,\n): Promise<CaptureSummary | null> {\n const durations = await storyDurations(args);\n const slices = planShards(partition.work, capturesOf, args.options.shardSize, (storyId) =>\n durations.get(storyId),\n );\n return slices.length === 0\n ? null\n : await commitPlan({ ...args, timed: durations.size > 0 }, slices, partition);\n}\n\nasync function commitPlan(\n args: FanOutInput,\n slices: EngineWork[][],\n partition: { work: EngineWork[]; inherited: InheritedStory[] },\n): Promise<CaptureSummary> {\n await supersedeEarlierRuns(args);\n const ctx = contextOf(args, {\n viewports: args.viewports,\n browsers: args.browsers,\n inherited: partition.inherited,\n progress: args.input.progress,\n });\n const failedInherits = await persistInherited(ctx);\n const storyIds = new Set([\n ...partition.work.flatMap((engine) => engine.stories.map((story) => story.id)),\n ...partition.inherited.map((inherited) => inherited.story.id),\n ]);\n const fresh = [...(await freshBrowsersOf(ctx))];\n const failed = new Set([...failedInherits, ...(args.testFailed ?? [])]);\n const rows = await recordPlan(args, slices, { fresh, storyIds, failed });\n await handOut(args, rows);\n await emitAttemptLog(args.input.recordLog, args.logger, \"info\", \"capture planned\", {\n shards: rows.length,\n stories: storyIds.size,\n ordering: args.timed ? \"longest-first\" : \"by-capture-count\",\n });\n return { storyCount: storyIds.size, failedCount: failed.size, shards: rows.length };\n}\n\n/**\n * A new plan of a build (recovery requeue, retry, a redelivered plan job) starts clean: the\n * snapshots an earlier plan or its shards wrote are dropped, so inherited rows are not doubled,\n * and the attempts it left open are closed, since only the newest plan's attempt is ever closed.\n */\nasync function supersedeEarlierRuns(args: FanOutInput): Promise<void> {\n const { db, tables } = args.options;\n await new SnapshotModel(db, tables).removeForBuild(args.build.id);\n await new CaptureAttemptModel(db, tables).closeOthers(\n args.build.id,\n args.input.attempt?.id ?? null,\n \"superseded by a new capture plan\",\n );\n}\n\nasync function recordPlan(\n args: FanOutInput,\n slices: EngineWork[][],\n plan: { fresh: string[]; storyIds: Set<string>; failed: ReadonlySet<string> },\n): Promise<CaptureShard[]> {\n const shards = new CaptureShardModel(args.options.db);\n await shards.clearBuild(args.build.id);\n const settings: PlanPayload = {\n browsers: args.browsers,\n fresh: plan.fresh,\n shardSize: args.options.shardSize ?? 0,\n total: slices.length,\n };\n const result: PlanResult = { storyIds: [...plan.storyIds], failedStoryIds: [...plan.failed] };\n return await shards.createPlan(\n args.project.id,\n args.build.id,\n {\n attemptId: args.input.attempt?.id ?? null,\n payload: JSON.stringify(settings),\n result: JSON.stringify(result),\n },\n slices.map((browsers) => ({ payload: shardPayload(args, browsers, plan.fresh) })),\n );\n}\n\nfunction shardPayload(args: FanOutInput, browsers: EngineWork[], fresh: string[]): string {\n const payload: ShardPayload = { browsers, viewports: args.viewports, fresh };\n return JSON.stringify(payload);\n}\n\n/** Enqueue every shard in order; the caller marks the build failed if the queue refuses one. */\nasync function handOut(args: FanOutInput, rows: CaptureShard[]): Promise<void> {\n const { enqueue } = args.options;\n for (const row of rows) {\n // oxlint-disable-next-line no-await-in-loop -- shards are queued in plan order\n await enqueue?.({\n buildId: args.build.id,\n shard: row.id,\n reqId: args.input.reqId,\n traceparent: args.input.traceparent,\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../../models/build.mjs";
|
|
5
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
6
|
+
//#region src/capture/shards/history.ts
|
|
7
|
+
/** What the project's recent builds say a story costs, so a plan can order its work by it. */
|
|
8
|
+
/**
|
|
9
|
+
* Milliseconds each story cost, taken from the newest build of this project whose snapshots carry
|
|
10
|
+
* timings. Older builds are a fair guide: story render cost moves with the story, not the build.
|
|
11
|
+
*
|
|
12
|
+
* Returns an empty map when nothing has been timed yet — a fresh project, a runner that reports no
|
|
13
|
+
* durations, or a retention purge that took the timed builds with it — and the plan then falls back
|
|
14
|
+
* to slicing by capture count alone.
|
|
15
|
+
*/
|
|
16
|
+
async function storyDurations(target) {
|
|
17
|
+
const { options, project, build } = target;
|
|
18
|
+
const builds = new BuildModel(options.db, options.tables);
|
|
19
|
+
const snapshots = new SnapshotModel(options.db, options.tables);
|
|
20
|
+
const recent = (await builds.list(project.id, { limit: BUILDS_SCANNED + 1 })).filter((candidate) => candidate.id !== build.id).slice(0, BUILDS_SCANNED);
|
|
21
|
+
for (const candidate of recent) {
|
|
22
|
+
const timings = await snapshots.durationsByStory(candidate.id);
|
|
23
|
+
if (timings.size > 0) return timings;
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ new Map();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Recent builds to look through for timings. More than a couple is wasted work: a build that
|
|
29
|
+
* rendered nothing (everything inherited) carries none, but several in a row do not happen.
|
|
30
|
+
*/
|
|
31
|
+
const BUILDS_SCANNED = 3;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { storyDurations };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=history.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.mjs","names":[],"sources":["../../../src/capture/shards/history.ts"],"sourcesContent":["/** What the project's recent builds say a story costs, so a plan can order its work by it. */\nimport { BuildModel } from \"../../models/build.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { ShardTarget } from \"./context.ts\";\n\n/**\n * Milliseconds each story cost, taken from the newest build of this project whose snapshots carry\n * timings. Older builds are a fair guide: story render cost moves with the story, not the build.\n *\n * Returns an empty map when nothing has been timed yet — a fresh project, a runner that reports no\n * durations, or a retention purge that took the timed builds with it — and the plan then falls back\n * to slicing by capture count alone.\n */\nexport async function storyDurations(target: ShardTarget): Promise<Map<string, number>> {\n const { options, project, build } = target;\n const builds = new BuildModel(options.db, options.tables);\n const snapshots = new SnapshotModel(options.db, options.tables);\n // One extra in case the build being planned is among the newest.\n const recent = (await builds.list(project.id, { limit: BUILDS_SCANNED + 1 }))\n .filter((candidate) => candidate.id !== build.id)\n .slice(0, BUILDS_SCANNED);\n for (const candidate of recent) {\n // oxlint-disable-next-line no-await-in-loop -- stops at the first build that carries timings\n const timings = await snapshots.durationsByStory(candidate.id);\n if (timings.size > 0) {\n return timings;\n }\n }\n return new Map();\n}\n\n/**\n * Recent builds to look through for timings. More than a couple is wasted work: a build that\n * rendered nothing (everything inherited) carries none, but several in a row do not happen.\n */\nconst BUILDS_SCANNED = 3;\n"],"mappings":";;;;;;;;;;;;;;;AAaA,eAAsB,eAAe,QAAmD;CACtF,MAAM,EAAE,SAAS,SAAS,UAAU;CACpC,MAAM,SAAS,IAAI,WAAW,QAAQ,IAAI,QAAQ,MAAM;CACxD,MAAM,YAAY,IAAI,cAAc,QAAQ,IAAI,QAAQ,MAAM;CAE9D,MAAM,UAAU,MAAM,OAAO,KAAK,QAAQ,IAAI,EAAE,OAAO,iBAAiB,EAAE,CAAC,EAAA,CACxE,QAAQ,cAAc,UAAU,OAAO,MAAM,EAAE,CAAC,CAChD,MAAM,GAAG,cAAc;CAC1B,KAAK,MAAM,aAAa,QAAQ;EAE9B,MAAM,UAAU,MAAM,UAAU,iBAAiB,UAAU,EAAE;EAC7D,IAAI,QAAQ,OAAO,GACjB,OAAO;CAEX;CACA,uBAAO,IAAI,IAAI;AACjB;;;;;AAMA,MAAM,iBAAiB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../../models/build.mjs";
|
|
5
|
+
import { finalizeBuild } from "../pipeline.mjs";
|
|
6
|
+
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
7
|
+
import { contextOf } from "./context.mjs";
|
|
8
|
+
//#region src/capture/shards/reduce.ts
|
|
9
|
+
/** The reduce stage: when the last render shard ends, exactly one worker finishes the build. */
|
|
10
|
+
/**
|
|
11
|
+
* When every render shard has ended, claim the finalize (exactly one worker can) and finish the
|
|
12
|
+
* build from the plan and the shards' outcomes. Returns the summary for the winner, else null.
|
|
13
|
+
*
|
|
14
|
+
* A finalize that throws gives its claim back, so the next run to reduce the build (a redelivery
|
|
15
|
+
* of the same shard) finalizes it instead of finding the claim taken. On a last try (`final`)
|
|
16
|
+
* nothing will come back for it, so the build is failed and the failure reported as the summary.
|
|
17
|
+
*/
|
|
18
|
+
async function reduceBuild(target, final = true) {
|
|
19
|
+
const shards = new CaptureShardModel(target.options.db);
|
|
20
|
+
const rows = await closedShards(shards, target.build.id);
|
|
21
|
+
if (!rows || !await shards.claimFinalize(target.project.id, target.build.id)) return null;
|
|
22
|
+
return await finalizeClaimed(target, shards, rows, final);
|
|
23
|
+
}
|
|
24
|
+
/** Finalize under the claim this run holds; on failure, give the claim back. */
|
|
25
|
+
async function finalizeClaimed(target, shards, rows, final) {
|
|
26
|
+
const plan = await shards.plan(target.build.id);
|
|
27
|
+
try {
|
|
28
|
+
const summary = await finalizeSharded(target, rows, plan);
|
|
29
|
+
await shards.finishFinalize(target.build.id);
|
|
30
|
+
return summary;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
await shards.releaseFinalize(target.build.id);
|
|
33
|
+
if (!final) throw error;
|
|
34
|
+
return await failFinalize(target, plan, error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The build's render shards once every one has ended, else null (none, or some still open). */
|
|
38
|
+
async function closedShards(shards, buildId) {
|
|
39
|
+
const rows = await shards.renderShards(buildId);
|
|
40
|
+
const open = rows.some((row) => row.status === "pending" || row.status === "running");
|
|
41
|
+
return rows.length === 0 || open ? null : rows;
|
|
42
|
+
}
|
|
43
|
+
async function finalizeSharded(target, rows, plan) {
|
|
44
|
+
const planResult = planResultOf(plan);
|
|
45
|
+
const sets = collectSets(rows, planResult);
|
|
46
|
+
await finalizeBuild(contextOf(target, {
|
|
47
|
+
viewports: [],
|
|
48
|
+
browsers: []
|
|
49
|
+
}), new Set(planResult.storyIds ?? []), sets.failed, sets.flaky, sets.a11y);
|
|
50
|
+
const finished = await new BuildModel(target.options.db, target.options.tables).get(target.build.id);
|
|
51
|
+
return {
|
|
52
|
+
storyCount: planResult.storyIds?.length ?? 0,
|
|
53
|
+
failedCount: sets.failed.size,
|
|
54
|
+
buildStatus: finished?.status ?? "failed",
|
|
55
|
+
attemptId: plan?.attemptId ?? null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The last try at finalizing failed: fail the build, so it does not sit in `capturing`. */
|
|
59
|
+
async function failFinalize(target, plan, error) {
|
|
60
|
+
await new BuildModel(target.options.db, target.options.tables).setStatus(target.build.id, "failed");
|
|
61
|
+
return {
|
|
62
|
+
storyCount: planResultOf(plan).storyIds?.length ?? 0,
|
|
63
|
+
failedCount: 0,
|
|
64
|
+
buildStatus: "failed",
|
|
65
|
+
attemptId: plan?.attemptId ?? null,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function planResultOf(plan) {
|
|
70
|
+
return JSON.parse(plan?.result ?? "{}");
|
|
71
|
+
}
|
|
72
|
+
/** Union the plan's and every shard's failures; a shard that ended `failed` fails all its stories. */
|
|
73
|
+
function collectSets(rows, plan) {
|
|
74
|
+
const union = {
|
|
75
|
+
failed: new Set(plan.failedStoryIds ?? []),
|
|
76
|
+
flaky: /* @__PURE__ */ new Set(),
|
|
77
|
+
a11y: /* @__PURE__ */ new Set()
|
|
78
|
+
};
|
|
79
|
+
for (const row of rows) if (row.status === "failed") addAll(union.failed, storyIdsOf(row));
|
|
80
|
+
else addShardResult(union, JSON.parse(row.result ?? "{}"));
|
|
81
|
+
return union;
|
|
82
|
+
}
|
|
83
|
+
function addShardResult(union, result) {
|
|
84
|
+
addAll(union.failed, result.failedStoryIds ?? []);
|
|
85
|
+
addAll(union.flaky, result.flakyStoryIds ?? []);
|
|
86
|
+
addAll(union.a11y, result.a11yStoryIds ?? []);
|
|
87
|
+
}
|
|
88
|
+
function addAll(into, ids) {
|
|
89
|
+
for (const id of ids) into.add(id);
|
|
90
|
+
}
|
|
91
|
+
function storyIdsOf(row) {
|
|
92
|
+
return (JSON.parse(row.payload ?? "{}").browsers ?? []).flatMap((engine) => engine.stories.map((story) => story.id));
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { reduceBuild };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=reduce.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reduce.mjs","names":[],"sources":["../../../src/capture/shards/reduce.ts"],"sourcesContent":["/** The reduce stage: when the last render shard ends, exactly one worker finishes the build. */\nimport { BuildModel } from \"../../models/build.ts\";\nimport { CaptureShardModel } from \"../../models/capture-shard.ts\";\nimport type { CaptureShard } from \"../../schema/capture-shard.ts\";\nimport type { BuildStatus } from \"../../types.ts\";\nimport { finalizeBuild } from \"../pipeline.ts\";\nimport type { PlanResult, ShardPayload, ShardResult } from \"../shard-plan.ts\";\nimport { contextOf, type ShardTarget } from \"./context.ts\";\n\n/** The finished build, reported by whichever worker finalized it. */\nexport interface FinalSummary {\n storyCount: number;\n failedCount: number;\n buildStatus: BuildStatus;\n /** The attempt row the plan was made under, to be closed by the caller. */\n attemptId: string | null;\n /** Set when the finalize itself failed on a last try, and the build was failed for it. */\n error?: string;\n}\n\n/**\n * When every render shard has ended, claim the finalize (exactly one worker can) and finish the\n * build from the plan and the shards' outcomes. Returns the summary for the winner, else null.\n *\n * A finalize that throws gives its claim back, so the next run to reduce the build (a redelivery\n * of the same shard) finalizes it instead of finding the claim taken. On a last try (`final`)\n * nothing will come back for it, so the build is failed and the failure reported as the summary.\n */\nexport async function reduceBuild(target: ShardTarget, final = true): Promise<FinalSummary | null> {\n const shards = new CaptureShardModel(target.options.db);\n const rows = await closedShards(shards, target.build.id);\n if (!rows || !(await shards.claimFinalize(target.project.id, target.build.id))) {\n return null;\n }\n return await finalizeClaimed(target, shards, rows, final);\n}\n\n/** Finalize under the claim this run holds; on failure, give the claim back. */\nasync function finalizeClaimed(\n target: ShardTarget,\n shards: CaptureShardModel,\n rows: CaptureShard[],\n final: boolean,\n): Promise<FinalSummary> {\n const plan = await shards.plan(target.build.id);\n try {\n const summary = await finalizeSharded(target, rows, plan);\n await shards.finishFinalize(target.build.id);\n return summary;\n } catch (error) {\n await shards.releaseFinalize(target.build.id);\n if (!final) {\n throw error;\n }\n return await failFinalize(target, plan, error);\n }\n}\n\n/** The build's render shards once every one has ended, else null (none, or some still open). */\nasync function closedShards(\n shards: CaptureShardModel,\n buildId: string,\n): Promise<CaptureShard[] | null> {\n const rows = await shards.renderShards(buildId);\n const open = rows.some((row) => row.status === \"pending\" || row.status === \"running\");\n return rows.length === 0 || open ? null : rows;\n}\n\nasync function finalizeSharded(\n target: ShardTarget,\n rows: CaptureShard[],\n plan: CaptureShard | null,\n): Promise<FinalSummary> {\n const planResult = planResultOf(plan);\n const sets = collectSets(rows, planResult);\n await finalizeBuild(\n contextOf(target, { viewports: [], browsers: [] }),\n new Set(planResult.storyIds ?? []),\n sets.failed,\n sets.flaky,\n sets.a11y,\n );\n const finished = await new BuildModel(target.options.db, target.options.tables).get(\n target.build.id,\n );\n return {\n storyCount: planResult.storyIds?.length ?? 0,\n failedCount: sets.failed.size,\n buildStatus: finished?.status ?? \"failed\",\n attemptId: plan?.attemptId ?? null,\n };\n}\n\n/** The last try at finalizing failed: fail the build, so it does not sit in `capturing`. */\nasync function failFinalize(\n target: ShardTarget,\n plan: CaptureShard | null,\n error: unknown,\n): Promise<FinalSummary> {\n await new BuildModel(target.options.db, target.options.tables).setStatus(\n target.build.id,\n \"failed\",\n );\n return {\n storyCount: planResultOf(plan).storyIds?.length ?? 0,\n failedCount: 0,\n buildStatus: \"failed\",\n attemptId: plan?.attemptId ?? null,\n error: error instanceof Error ? error.message : String(error),\n };\n}\n\nfunction planResultOf(plan: CaptureShard | null): Partial<PlanResult> {\n return JSON.parse(plan?.result ?? \"{}\") as Partial<PlanResult>;\n}\n\ninterface FailureUnion {\n failed: Set<string>;\n flaky: Set<string>;\n a11y: Set<string>;\n}\n\n/** Union the plan's and every shard's failures; a shard that ended `failed` fails all its stories. */\nfunction collectSets(rows: CaptureShard[], plan: Partial<PlanResult>): FailureUnion {\n const union: FailureUnion = {\n failed: new Set(plan.failedStoryIds ?? []),\n flaky: new Set(),\n a11y: new Set(),\n };\n for (const row of rows) {\n if (row.status === \"failed\") {\n addAll(union.failed, storyIdsOf(row));\n } else {\n addShardResult(union, JSON.parse(row.result ?? \"{}\") as Partial<ShardResult>);\n }\n }\n return union;\n}\n\nfunction addShardResult(union: FailureUnion, result: Partial<ShardResult>): void {\n addAll(union.failed, result.failedStoryIds ?? []);\n addAll(union.flaky, result.flakyStoryIds ?? []);\n addAll(union.a11y, result.a11yStoryIds ?? []);\n}\n\nfunction addAll(into: Set<string>, ids: readonly string[]): void {\n for (const id of ids) {\n into.add(id);\n }\n}\n\nfunction storyIdsOf(row: CaptureShard): string[] {\n const payload = JSON.parse(row.payload ?? \"{}\") as Partial<ShardPayload>;\n return (payload.browsers ?? []).flatMap((engine) => engine.stories.map((story) => story.id));\n}\n"],"mappings":"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"}
|
package/dist/capture/sizing.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { sha256 } from "../utils/hash.mjs";
|
|
5
4
|
//#region src/capture/sizing.ts
|
|
6
5
|
/** Padding around the cropped component (8px each side). */
|
|
7
6
|
const PAD = 16;
|
|
@@ -104,16 +103,7 @@ function parsedViewports(raw) {
|
|
|
104
103
|
} catch {}
|
|
105
104
|
return null;
|
|
106
105
|
}
|
|
107
|
-
/** Hash infra determinism: browser + viewports + sizing defaults. */
|
|
108
|
-
function infraHashFor(browser, viewports, defaults = SIZING_DEFAULTS) {
|
|
109
|
-
const payload = JSON.stringify({
|
|
110
|
-
browser,
|
|
111
|
-
viewports,
|
|
112
|
-
defaults
|
|
113
|
-
});
|
|
114
|
-
return sha256(payload);
|
|
115
|
-
}
|
|
116
106
|
//#endregion
|
|
117
|
-
export { SIZING_DEFAULTS, getScreenshotPlan,
|
|
107
|
+
export { SIZING_DEFAULTS, getScreenshotPlan, resolveViewports };
|
|
118
108
|
|
|
119
109
|
//# sourceMappingURL=sizing.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sizing.mjs","names":[],"sources":["../../src/capture/sizing.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"sizing.mjs","names":[],"sources":["../../src/capture/sizing.ts"],"sourcesContent":["import type { Viewport } from \"./adapter.ts\";\n\n/** Padding around the cropped component (8px each side). */\nconst PAD = 16;\nconst HALF_PAD = 8;\n\n/** Default min/max dimensions for cropped/fullPage screenshots (diff stability). */\nexport const SIZING_DEFAULTS = {\n minWidth: 100,\n minHeight: 100,\n maxWidth: 2000,\n maxHeight: 4000,\n whitespaceThreshold: 0.6,\n} as const;\n\n/** Bounding box of #storybook-root as returned by Playwright/Puppeteer. */\nexport interface BoundingBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Options for computing the screenshot plan. */\nexport interface SizingOptions {\n /** Per-story override: false forces full viewport, true forces crop when possible. */\n autoCrop?: boolean;\n /** Whitespace fraction (0..1) above which we crop; default 0.6. */\n whitespaceThreshold?: number;\n /** Whether overflow should trigger fullPage; default true. */\n fullPageOnOverflow?: boolean;\n /** Min dimensions for the final PNG (clamp after padding). */\n minWidth?: number;\n minHeight?: number;\n /** Max dimensions for the final PNG (clamp after padding). */\n maxWidth?: number;\n maxHeight?: number;\n}\n\n/** Result of the sizing decision — what the runner should do. */\nexport interface ScreenshotPlan {\n /** Viewport to set on the page before capture. */\n viewport: Viewport;\n /** Clip to use for page.screenshot({clip}). Undefined means full viewport. */\n clip?: { x: number; y: number; width: number; height: number };\n /** Whether to use fullPage screenshot (for overflow). */\n fullPage: boolean;\n}\n\n/** Clamp value between min and max. */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n\nfunction isOverflowing(box: BoundingBox, viewport: Viewport): boolean {\n return box.width > viewport.width || box.height > viewport.height;\n}\n\nfunction whitespaceOf(box: BoundingBox, viewport: Viewport): number {\n const viewportArea = viewport.width * viewport.height;\n const boxArea = box.width * box.height;\n const coverage = viewportArea > 0 ? boxArea / viewportArea : 1;\n return 1 - Math.min(coverage, 1);\n}\n\nfunction shouldCrop(\n box: BoundingBox,\n viewport: Viewport,\n opts: SizingOptions,\n threshold: number,\n): boolean {\n if (opts.autoCrop === true) return true;\n return whitespaceOf(box, viewport) > threshold;\n}\n\nfunction paddedClip(\n box: BoundingBox,\n minW: number,\n maxW: number,\n minH: number,\n maxH: number,\n): BoundingBox {\n const w = clamp(Math.ceil(box.width + PAD), minW, maxW);\n const h = clamp(Math.ceil(box.height + PAD), minH, maxH);\n return {\n x: Math.max(0, box.x - HALF_PAD),\n y: Math.max(0, box.y - HALF_PAD),\n width: w,\n height: h,\n };\n}\n\n/**\n * Compute the screenshot plan for a single story×viewport.\n * Single prod owner for sizing logic — runners import this, never duplicate.\n */\nexport function getScreenshotPlan(\n viewport: Viewport,\n box: BoundingBox | null,\n options: SizingOptions = {},\n): ScreenshotPlan {\n if (options.autoCrop === false) return { viewport, fullPage: false };\n if (!box) return { viewport, fullPage: false };\n if (isOverflowing(box, viewport) && (options.fullPageOnOverflow ?? true)) {\n return { viewport, fullPage: true };\n }\n if (isOverflowing(box, viewport)) {\n return grownViewportPlan(viewport, box, options);\n }\n return maybeCroppedPlan(viewport, box, options);\n}\n\nfunction grownViewportPlan(\n viewport: Viewport,\n box: BoundingBox,\n options: SizingOptions,\n): ScreenshotPlan {\n const maxH = options.maxHeight ?? SIZING_DEFAULTS.maxHeight;\n const grownHeight = clamp(Math.ceil(box.height + PAD), viewport.height, maxH);\n return { viewport: { ...viewport, height: grownHeight }, fullPage: false };\n}\n\nfunction maybeCroppedPlan(\n viewport: Viewport,\n box: BoundingBox,\n options: SizingOptions,\n): ScreenshotPlan {\n const threshold = options.whitespaceThreshold ?? SIZING_DEFAULTS.whitespaceThreshold;\n if (!shouldCrop(box, viewport, options, threshold)) return { viewport, fullPage: false };\n const minW = options.minWidth ?? SIZING_DEFAULTS.minWidth;\n const minH = options.minHeight ?? SIZING_DEFAULTS.minHeight;\n const maxW = options.maxWidth ?? SIZING_DEFAULTS.maxWidth;\n const maxH = options.maxHeight ?? SIZING_DEFAULTS.maxHeight;\n const clip = paddedClip(box, minW, maxW, minH, maxH);\n return {\n viewport,\n clip: { x: Math.round(clip.x), y: Math.round(clip.y), width: clip.width, height: clip.height },\n fullPage: false,\n };\n}\n\n/** Resolve viewports from project/server config with fallback to default (not sizing, just list). */\nexport function resolveViewports(\n rawViewports: string | null | undefined,\n fallback: Viewport[],\n serverViewports?: Viewport[],\n): Viewport[] {\n if (serverViewports && serverViewports.length > 0) return serverViewports;\n if (rawViewports) return parsedViewports(rawViewports) ?? fallback;\n return fallback;\n}\n\nfunction parsedViewports(raw: string): Viewport[] | null {\n try {\n const parsed = JSON.parse(raw) as Viewport[];\n if (Array.isArray(parsed) && parsed.length > 0) return parsed;\n } catch {\n // fallback\n }\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -2,75 +2,12 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../models/build.mjs";
|
|
5
|
-
import { StatusConfigModel } from "../models/status-config.mjs";
|
|
6
5
|
//#region src/capture/skip-checks.ts
|
|
7
6
|
/** Skip capture when another approved build already covers the same commit. */
|
|
8
7
|
async function hasApprovedBuildForSha(db, tables, projectId, sha, excludeBuildId) {
|
|
9
|
-
return (await new BuildModel(db, tables).list(projectId)).some((
|
|
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.tables, 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;
|
|
8
|
+
return (await new BuildModel(db, tables).list(projectId, { status: "approved" })).some((build) => build.gitSha === sha && build.id !== excludeBuildId);
|
|
72
9
|
}
|
|
73
10
|
//#endregion
|
|
74
|
-
export { hasApprovedBuildForSha
|
|
11
|
+
export { hasApprovedBuildForSha };
|
|
75
12
|
|
|
76
13
|
//# sourceMappingURL=skip-checks.mjs.map
|