@storyshelf/app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { notifyProjectWith, notifySystemWith } from "./notify.mjs";
|
|
5
|
+
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
6
|
+
//#region src/capture-alerts.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds already alerted about, keyed by the build's last update. A sharded build settles one job
|
|
9
|
+
* per shard and every one of them sees the failed build; only the first alerts. A retry rewrites
|
|
10
|
+
* the build, so its next failure is a new key.
|
|
11
|
+
*/
|
|
12
|
+
const alerted = /* @__PURE__ */ new Set();
|
|
13
|
+
function alreadyAlerted(build) {
|
|
14
|
+
const key = `${build.id}@${build.updatedAt}`;
|
|
15
|
+
if (alerted.has(key)) return true;
|
|
16
|
+
alerted.add(key);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/** Alert admins when a capture leaves its build failed. */
|
|
20
|
+
async function notifyCaptureFailed(options, config, logger, buildId, publicOrigin) {
|
|
21
|
+
const build = await new BuildModel(options.database).get(buildId);
|
|
22
|
+
if (build?.status !== "failed" || alreadyAlerted(build)) return;
|
|
23
|
+
const deps = {
|
|
24
|
+
db: options.database,
|
|
25
|
+
config,
|
|
26
|
+
ui: options.ui ?? {},
|
|
27
|
+
logger,
|
|
28
|
+
notifiers: options.notifications?.notifiers ?? [],
|
|
29
|
+
notificationsConfig: options.notifications?.config,
|
|
30
|
+
publicOrigin: publicOrigin ? { url: publicOrigin } : void 0
|
|
31
|
+
};
|
|
32
|
+
await notifySystemWith(deps, "sys:capture-failed", {
|
|
33
|
+
buildId,
|
|
34
|
+
projectId: build.projectId,
|
|
35
|
+
gitBranch: build.gitBranch
|
|
36
|
+
});
|
|
37
|
+
const project = await new ProjectModel(options.database).get(build.projectId);
|
|
38
|
+
if (project) await notifyProjectWith(deps, project, "build:failed", {
|
|
39
|
+
buildId,
|
|
40
|
+
status: "failed"
|
|
41
|
+
}, `/builds/${buildId}`);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { alreadyAlerted, notifyCaptureFailed };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=capture-alerts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-alerts.mjs","names":[],"sources":["../src/capture-alerts.ts"],"sourcesContent":["/** Admin alerts for captures that leave their build failed; the app, not the capture child, sends them. */\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport { notifyProjectWith, notifySystemWith } from \"./notify.ts\";\n\n/**\n * Builds already alerted about, keyed by the build's last update. A sharded build settles one job\n * per shard and every one of them sees the failed build; only the first alerts. A retry rewrites\n * the build, so its next failure is a new key.\n */\nconst alerted = new Set<string>();\n\nexport function alreadyAlerted(build: { id: string; updatedAt: string }): boolean {\n const key = `${build.id}@${build.updatedAt}`;\n if (alerted.has(key)) {\n return true;\n }\n alerted.add(key);\n return false;\n}\n\n/** Alert admins when a capture leaves its build failed. */\nexport async function notifyCaptureFailed(\n options: ShelfOptions,\n config: ShelfConfig,\n logger: Logger,\n buildId: string,\n publicOrigin?: () => Promise<string | undefined>,\n): Promise<void> {\n const build = await new BuildModel(options.database).get(buildId);\n if (build?.status !== \"failed\" || alreadyAlerted(build)) {\n return;\n }\n const deps = {\n db: options.database,\n config,\n ui: options.ui ?? {},\n logger,\n notifiers: options.notifications?.notifiers ?? [],\n notificationsConfig: options.notifications?.config,\n publicOrigin: publicOrigin ? { url: publicOrigin } : undefined,\n };\n await notifySystemWith(deps, \"sys:capture-failed\", {\n buildId,\n projectId: build.projectId,\n gitBranch: build.gitBranch,\n });\n // Subscribers of the project's `build:failed` topic hear of a failed capture too.\n const project = await new ProjectModel(options.database).get(build.projectId);\n if (project) {\n await notifyProjectWith(\n deps,\n project,\n \"build:failed\",\n { buildId, status: \"failed\" },\n `/builds/${buildId}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,0BAAU,IAAI,IAAY;AAEhC,SAAgB,eAAe,OAAmD;CAChF,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG,MAAM;CACjC,IAAI,QAAQ,IAAI,GAAG,GACjB,OAAO;CAET,QAAQ,IAAI,GAAG;CACf,OAAO;AACT;;AAGA,eAAsB,oBACpB,SACA,QACA,QACA,SACA,cACe;CACf,MAAM,QAAQ,MAAM,IAAI,WAAW,QAAQ,QAAQ,CAAC,CAAC,IAAI,OAAO;CAChE,IAAI,OAAO,WAAW,YAAY,eAAe,KAAK,GACpD;CAEF,MAAM,OAAO;EACX,IAAI,QAAQ;EACZ;EACA,IAAI,QAAQ,MAAM,CAAC;EACnB;EACA,WAAW,QAAQ,eAAe,aAAa,CAAC;EAChD,qBAAqB,QAAQ,eAAe;EAC5C,cAAc,eAAe,EAAE,KAAK,aAAa,IAAI,KAAA;CACvD;CACA,MAAM,iBAAiB,MAAM,sBAAsB;EACjD;EACA,WAAW,MAAM;EACjB,WAAW,MAAM;CACnB,CAAC;CAED,MAAM,UAAU,MAAM,IAAI,aAAa,QAAQ,QAAQ,CAAC,CAAC,IAAI,MAAM,SAAS;CAC5E,IAAI,SACF,MAAM,kBACJ,MACA,SACA,gBACA;EAAE;EAAS,QAAQ;CAAS,GAC5B,WAAW,SACb;AAEJ"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { DIFF_WORKERS_ENV, IMAGE_ENGINE_ENV } from "@storyshelf/core/diff";
|
|
5
|
+
import { detectResources } from "@storyshelf/core/utils";
|
|
6
|
+
import { CAPTURE_RETRIES_ENV, CAPTURE_SHARD_SIZE_ENV, canForkCaptureChild, createForkedCaptureHandler, decideCaptureProcess, forkCaptureChild } from "@storyshelf/core/capture";
|
|
7
|
+
//#region src/capture-process.ts
|
|
8
|
+
/** Chooses between in-process and forked capture, and builds the forked handler. */
|
|
9
|
+
/**
|
|
10
|
+
* Whether to fork a capture child. `mode: "fork"` forces it (and fails if it cannot); `auto` forks
|
|
11
|
+
* when a `workerEntry` is set and the host can afford a second process.
|
|
12
|
+
*
|
|
13
|
+
* @throws When `mode` is `"fork"` but the runtime cannot fork.
|
|
14
|
+
*/
|
|
15
|
+
function shouldForkCapture(testing, logger) {
|
|
16
|
+
if (!testing?.workerEntry || testing.mode === "in-process") return false;
|
|
17
|
+
if (testing.mode === "fork") return requireFork();
|
|
18
|
+
const decision = decideForHost();
|
|
19
|
+
if (decision.mode === "in-process") logger.info({ reason: decision.reason }, "capture workerEntry set; capturing in-process");
|
|
20
|
+
return decision.mode === "fork";
|
|
21
|
+
}
|
|
22
|
+
function requireFork() {
|
|
23
|
+
if (!canForkCaptureChild()) throw new Error("testing.mode \"fork\" is set but child processes are unavailable here");
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function decideForHost() {
|
|
27
|
+
const { cpus, memoryBytes } = detectResources();
|
|
28
|
+
return decideCaptureProcess({
|
|
29
|
+
cpus,
|
|
30
|
+
memoryBytes,
|
|
31
|
+
canFork: canForkCaptureChild()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Settings the parent hands to a forked child through its environment. */
|
|
35
|
+
function childEnvOf(deps) {
|
|
36
|
+
const env = { [CAPTURE_RETRIES_ENV]: String(deps.retries) };
|
|
37
|
+
if (deps.shardSize !== void 0) env[CAPTURE_SHARD_SIZE_ENV] = String(deps.shardSize);
|
|
38
|
+
if (deps.imageEngine !== void 0) env[IMAGE_ENGINE_ENV] = deps.imageEngine;
|
|
39
|
+
if (deps.diffThreads !== void 0) env[DIFF_WORKERS_ENV] = String(deps.diffThreads);
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
/** The `capture` handler that runs jobs in a forked child, with the in-process handler as its fallback. */
|
|
43
|
+
function createForkedHandler(deps) {
|
|
44
|
+
const env = childEnvOf(deps);
|
|
45
|
+
return createForkedCaptureHandler({
|
|
46
|
+
spawn: () => forkCaptureChild({
|
|
47
|
+
entry: deps.workerEntry,
|
|
48
|
+
env
|
|
49
|
+
}),
|
|
50
|
+
fallback: deps.fallback,
|
|
51
|
+
maxRetries: deps.retries,
|
|
52
|
+
onCrashed: deps.onCrashed,
|
|
53
|
+
logger: deps.logger
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { childEnvOf, createForkedHandler, shouldForkCapture };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=capture-process.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-process.mjs","names":[],"sources":["../src/capture-process.ts"],"sourcesContent":["/** Chooses between in-process and forked capture, and builds the forked handler. */\nimport {\n canForkCaptureChild,\n CAPTURE_RETRIES_ENV,\n CAPTURE_SHARD_SIZE_ENV,\n createForkedCaptureHandler,\n decideCaptureProcess,\n forkCaptureChild,\n type ForkDecision,\n type ForkedCaptureHandler,\n type KindHandler,\n type ConsumerJob,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfTestingOptions } from \"@storyshelf/core/config\";\nimport { DIFF_WORKERS_ENV, IMAGE_ENGINE_ENV } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { detectResources } from \"@storyshelf/core/utils\";\n\n/**\n * Whether to fork a capture child. `mode: \"fork\"` forces it (and fails if it cannot); `auto` forks\n * when a `workerEntry` is set and the host can afford a second process.\n *\n * @throws When `mode` is `\"fork\"` but the runtime cannot fork.\n */\nexport function shouldForkCapture(\n testing: ShelfTestingOptions | undefined,\n logger: Logger,\n): boolean {\n if (!testing?.workerEntry || testing.mode === \"in-process\") {\n return false;\n }\n if (testing.mode === \"fork\") {\n return requireFork();\n }\n const decision = decideForHost();\n if (decision.mode === \"in-process\") {\n logger.info({ reason: decision.reason }, \"capture workerEntry set; capturing in-process\");\n }\n return decision.mode === \"fork\";\n}\n\nfunction requireFork(): true {\n if (!canForkCaptureChild()) {\n throw new Error('testing.mode \"fork\" is set but child processes are unavailable here');\n }\n return true;\n}\n\nfunction decideForHost(): ForkDecision {\n const { cpus, memoryBytes } = detectResources();\n return decideCaptureProcess({ cpus, memoryBytes, canFork: canForkCaptureChild() });\n}\n\n/** Dependencies for {@link createForkedHandler}. */\nexport interface ForkedHandlerDeps {\n workerEntry: string;\n /** Captures per shard for big builds, handed to the child (`testing.shardSize`). */\n shardSize?: number;\n /** `testing.images.engine`, handed to the child so it diffs with the same codec. */\n imageEngine?: string;\n /** `testing.compare.threads`, handed to the child's diff pool. */\n diffThreads?: number;\n /** `testing.retries`, handed to the child, which stamps it on the shard jobs it queues. */\n retries: number;\n /** Takes over if forking stops being viable. */\n fallback: KindHandler;\n /** A job the child kept killing on its last attempt: write its failure. */\n onCrashed: (job: ConsumerJob, error: string) => Promise<void>;\n logger: Logger;\n}\n\n/** Settings the parent hands to a forked child through its environment. */\nexport function childEnvOf(\n deps: Pick<ForkedHandlerDeps, \"shardSize\" | \"imageEngine\" | \"diffThreads\" | \"retries\">,\n): Record<string, string> {\n const env: Record<string, string> = { [CAPTURE_RETRIES_ENV]: String(deps.retries) };\n if (deps.shardSize !== undefined) {\n env[CAPTURE_SHARD_SIZE_ENV] = String(deps.shardSize);\n }\n if (deps.imageEngine !== undefined) {\n env[IMAGE_ENGINE_ENV] = deps.imageEngine;\n }\n if (deps.diffThreads !== undefined) {\n env[DIFF_WORKERS_ENV] = String(deps.diffThreads);\n }\n return env;\n}\n\n/** The `capture` handler that runs jobs in a forked child, with the in-process handler as its fallback. */\nexport function createForkedHandler(deps: ForkedHandlerDeps): ForkedCaptureHandler {\n const env = childEnvOf(deps);\n return createForkedCaptureHandler({\n spawn: () => forkCaptureChild({ entry: deps.workerEntry, env }),\n fallback: deps.fallback,\n maxRetries: deps.retries,\n onCrashed: deps.onCrashed,\n logger: deps.logger,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,kBACd,SACA,QACS;CACT,IAAI,CAAC,SAAS,eAAe,QAAQ,SAAS,cAC5C,OAAO;CAET,IAAI,QAAQ,SAAS,QACnB,OAAO,YAAY;CAErB,MAAM,WAAW,cAAc;CAC/B,IAAI,SAAS,SAAS,cACpB,OAAO,KAAK,EAAE,QAAQ,SAAS,OAAO,GAAG,+CAA+C;CAE1F,OAAO,SAAS,SAAS;AAC3B;AAEA,SAAS,cAAoB;CAC3B,IAAI,CAAC,oBAAoB,GACvB,MAAM,IAAI,MAAM,uEAAqE;CAEvF,OAAO;AACT;AAEA,SAAS,gBAA8B;CACrC,MAAM,EAAE,MAAM,gBAAgB,gBAAgB;CAC9C,OAAO,qBAAqB;EAAE;EAAM;EAAa,SAAS,oBAAoB;CAAE,CAAC;AACnF;;AAqBA,SAAgB,WACd,MACwB;CACxB,MAAM,MAA8B,GAAG,sBAAsB,OAAO,KAAK,OAAO,EAAE;CAClF,IAAI,KAAK,cAAc,KAAA,GACrB,IAAI,0BAA0B,OAAO,KAAK,SAAS;CAErD,IAAI,KAAK,gBAAgB,KAAA,GACvB,IAAI,oBAAoB,KAAK;CAE/B,IAAI,KAAK,gBAAgB,KAAA,GACvB,IAAI,oBAAoB,OAAO,KAAK,WAAW;CAEjD,OAAO;AACT;;AAGA,SAAgB,oBAAoB,MAA+C;CACjF,MAAM,MAAM,WAAW,IAAI;CAC3B,OAAO,2BAA2B;EAChC,aAAa,iBAAiB;GAAE,OAAO,KAAK;GAAa;EAAI,CAAC;EAC9D,UAAU,KAAK;EACf,YAAY,KAAK;EACjB,WAAW,KAAK;EAChB,QAAQ,KAAK;CACf,CAAC;AACH"}
|
package/dist/capture-setup.mjs
CHANGED
|
@@ -2,138 +2,116 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { credentialKeys } from "./credential-keys.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { notifyCaptureFailed } from "./capture-alerts.mjs";
|
|
6
|
+
import { createForkedHandler, shouldForkCapture } from "./capture-process.mjs";
|
|
7
|
+
import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
|
|
8
|
+
import { currentTraceparent } from "@storyshelf/observability";
|
|
9
|
+
import { DEFAULT_RETRIES, captureDispatchDeps, createCaptureHandler, failCrashedCapture, isFinalAttempt } from "@storyshelf/core/capture";
|
|
9
10
|
//#region src/capture-setup.ts
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function timedRunJob(runJob, job) {
|
|
21
|
-
const instruments = captureMetrics();
|
|
22
|
-
const start = performance.now();
|
|
23
|
-
try {
|
|
24
|
-
await runJob(job);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
instruments.jobsFailed.add(1);
|
|
27
|
-
instruments.jobDuration.record(performance.now() - start);
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
instruments.jobsCompleted.add(1);
|
|
31
|
-
instruments.jobDuration.record(performance.now() - start);
|
|
32
|
-
}
|
|
33
|
-
/** Alert admins when an in-process capture leaves its build failed. */
|
|
34
|
-
async function notifyCaptureFailed(options, config, logger, buildId) {
|
|
35
|
-
const build = await new BuildModel(options.database).get(buildId);
|
|
36
|
-
if (build?.status !== "failed") return;
|
|
37
|
-
await notifySystemWith({
|
|
38
|
-
db: options.database,
|
|
39
|
-
config,
|
|
40
|
-
ui: options.ui ?? {},
|
|
41
|
-
logger,
|
|
42
|
-
notifiers: options.notifiers ?? []
|
|
43
|
-
}, "sys:capture-failed", {
|
|
44
|
-
buildId,
|
|
45
|
-
projectId: build.projectId,
|
|
46
|
-
gitBranch: build.gitBranch
|
|
47
|
-
});
|
|
11
|
+
/** Queue a build, continuing the active request trace when present. */
|
|
12
|
+
function enqueuerFor(queue) {
|
|
13
|
+
return async (buildId, reqId) => {
|
|
14
|
+
const traceparent = currentTraceparent();
|
|
15
|
+
await queue.enqueue({
|
|
16
|
+
buildId,
|
|
17
|
+
reqId,
|
|
18
|
+
...traceparent ? { traceparent } : {}
|
|
19
|
+
});
|
|
20
|
+
};
|
|
48
21
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Wire capture for `options`: `testing` always has a runner, so this instance consumes the
|
|
24
|
+
* captures it queues. Shard jobs go back onto the same queue the capture came from.
|
|
25
|
+
*/
|
|
26
|
+
function setupCapture(options, config, delivery, logger, jobs) {
|
|
27
|
+
const testing = options.testing;
|
|
28
|
+
if (testing === void 0) return {
|
|
52
29
|
queue: null,
|
|
53
|
-
enqueueCapture: void 0
|
|
30
|
+
enqueueCapture: void 0,
|
|
31
|
+
handler: null
|
|
54
32
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
33
|
+
const queue = jobs.view;
|
|
34
|
+
const enqueueCapture = enqueuerFor(queue);
|
|
35
|
+
const handlerOptions = captureHandlerOptions(options, config, delivery, logger, {
|
|
36
|
+
runner: testing.runner,
|
|
37
|
+
enqueue: async (job) => {
|
|
38
|
+
await queue.enqueue(job);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const retries = options.testing?.retries ?? DEFAULT_RETRIES;
|
|
42
|
+
const dispatch = captureDispatchDeps(handlerOptions, retries);
|
|
43
|
+
const failed = async (job, error) => {
|
|
44
|
+
await failCrashedCapture(dispatch, job, error);
|
|
45
|
+
await notifyCaptureFailed(options, config, logger, job.buildId, delivery.publicOrigin);
|
|
46
|
+
};
|
|
47
|
+
jobs.exhausted.capture = failed;
|
|
48
|
+
const inProcess = createCaptureHandler(handlerOptions);
|
|
49
|
+
return {
|
|
50
|
+
queue,
|
|
51
|
+
enqueueCapture,
|
|
52
|
+
handler: alerting(shouldForkCapture(testing, logger) ? forkedHandler(options, {
|
|
53
|
+
fallback: inProcess,
|
|
54
|
+
retries,
|
|
55
|
+
onCrashed: failed,
|
|
56
|
+
logger
|
|
57
|
+
}) : inProcess, retries, async (buildId) => {
|
|
58
|
+
await notifyCaptureFailed(options, config, logger, buildId, delivery.publicOrigin);
|
|
59
|
+
})
|
|
76
60
|
};
|
|
77
|
-
|
|
78
|
-
|
|
61
|
+
}
|
|
62
|
+
function captureHandlerOptions(options, config, delivery, logger, parts) {
|
|
63
|
+
return {
|
|
79
64
|
db: options.database,
|
|
80
|
-
tables: {
|
|
81
|
-
projects: options.database.tables.projects,
|
|
82
|
-
builds: options.database.tables.builds,
|
|
83
|
-
buildLabels: options.database.tables.buildLabels,
|
|
84
|
-
snapshots: options.database.tables.snapshots,
|
|
85
|
-
baselines: options.database.tables.baselines,
|
|
86
|
-
captureAttempts: options.database.tables.captureAttempts,
|
|
87
|
-
captureLogs: options.database.tables.captureLogs
|
|
88
|
-
},
|
|
89
65
|
storage: options.storage,
|
|
90
|
-
runner:
|
|
91
|
-
scratchDir: config.scratchDir,
|
|
66
|
+
runner: parts.runner,
|
|
67
|
+
scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,
|
|
92
68
|
viewports: config.viewports,
|
|
93
69
|
logger,
|
|
94
|
-
secret: credentialKeys(config)
|
|
95
|
-
};
|
|
96
|
-
const dispatchDeps = {
|
|
97
|
-
db: options.database,
|
|
98
|
-
tables: {
|
|
99
|
-
projects: options.database.tables.projects,
|
|
100
|
-
builds: options.database.tables.builds,
|
|
101
|
-
buildLabels: options.database.tables.buildLabels,
|
|
102
|
-
snapshots: options.database.tables.snapshots,
|
|
103
|
-
projectStatusConfigs: options.database.tables.projectStatusConfigs,
|
|
104
|
-
captureAttempts: options.database.tables.captureAttempts,
|
|
105
|
-
captureLogs: options.database.tables.captureLogs
|
|
106
|
-
},
|
|
107
|
-
jobOptions,
|
|
108
|
-
gitHosts,
|
|
109
70
|
secret: credentialKeys(config),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await notifyCaptureFailed(options, config, logger, job.buildId);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
const enqueueCapture = async (buildId, reqId) => {
|
|
122
|
-
await enqueueJob(captureQueue, buildId, reqId);
|
|
71
|
+
design: options.design,
|
|
72
|
+
shardSize: options.testing?.shardSize,
|
|
73
|
+
notifiers: delivery.notifiers,
|
|
74
|
+
buildState: delivery.buildState,
|
|
75
|
+
publicOrigin: delivery.publicOrigin,
|
|
76
|
+
config,
|
|
77
|
+
enqueue: parts.enqueue,
|
|
78
|
+
maxRetries: options.testing?.retries
|
|
123
79
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
80
|
+
}
|
|
81
|
+
function forkedHandler(options, parts) {
|
|
82
|
+
const testing = options.testing;
|
|
83
|
+
return createForkedHandler({
|
|
84
|
+
workerEntry: testing?.workerEntry ?? "",
|
|
85
|
+
shardSize: testing?.shardSize,
|
|
86
|
+
imageEngine: testing?.images?.engine,
|
|
87
|
+
diffThreads: testing?.compare?.threads,
|
|
88
|
+
...parts
|
|
129
89
|
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Alert admins when a capture left its build failed: after a run that ended, and after a final
|
|
93
|
+
* attempt that threw (the build may then already be failed by a crash hook). Alerts stay in this
|
|
94
|
+
* process whichever process rendered.
|
|
95
|
+
*/
|
|
96
|
+
function alerting(inner, retries, alert) {
|
|
130
97
|
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
98
|
+
accepts: (job) => inner.accepts?.(job) !== false,
|
|
99
|
+
run: async (job, context) => {
|
|
100
|
+
try {
|
|
101
|
+
await inner.run(job, context);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (isFinalAttempt(job, retries)) await alert(job.buildId);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
await alert(job.buildId);
|
|
107
|
+
},
|
|
108
|
+
cancel: async (job) => {
|
|
109
|
+
await inner.cancel?.(job);
|
|
110
|
+
},
|
|
111
|
+
...inner.close ? { close: inner.close } : {}
|
|
134
112
|
};
|
|
135
113
|
}
|
|
136
114
|
//#endregion
|
|
137
|
-
export {
|
|
115
|
+
export { setupCapture };
|
|
138
116
|
|
|
139
117
|
//# sourceMappingURL=capture-setup.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-setup.mjs","names":[],"sources":["../src/capture-setup.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"capture-setup.mjs","names":[],"sources":["../src/capture-setup.ts"],"sourcesContent":["/**\n * Capture wiring (ADR 0006, decisions 4 to 6): where uploads queue their capture, and the handler\n * of `capture` jobs. Captures queue on the shared database queue. The handler runs them in this process, or in a forked child the parent pushes jobs\n * to (`testing.workerEntry`). The consumer that polls the queue and calls the handler is built\n * in `jobs-setup.ts`.\n */\nimport type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { CaptureRunner } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n captureDispatchDeps,\n createCaptureHandler,\n DEFAULT_RETRIES,\n failCrashedCapture,\n isFinalAttempt,\n type BuildStatePublisher,\n type CaptureHandlerOptions,\n type ConsumerJob,\n type KindHandler,\n} from \"@storyshelf/core/capture\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { currentTraceparent } from \"@storyshelf/observability\";\nimport { notifyCaptureFailed } from \"./capture-alerts.ts\";\nimport { createForkedHandler, shouldForkCapture } from \"./capture-process.ts\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport type { JobQueue } from \"./job-queue.ts\";\n\n/** The `capture` handler, with a way to stop a forked child once the consumer has drained. */\nexport interface CaptureHandler extends KindHandler {\n close?: () => Promise<void>;\n}\n\n/** What uploads and the consumer need of capture. */\nexport interface CaptureWiring {\n /** Where captures queue: the database queue, or null without `testing`. */\n queue: CaptureQueue | null;\n /** Queues a capture for a build; undefined without `testing`. */\n enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n /** Runs `capture` jobs here; null without `testing`. */\n handler: CaptureHandler | null;\n}\n\n/** What a capture hands its results to. */\ninterface Delivery {\n notifiers: NotifierProvider[];\n buildState: BuildStatePublisher;\n /** The resolved public origin (ADR 0007): status links and targets that need a URL use it. */\n publicOrigin: () => Promise<string | undefined>;\n}\n\n/** Queue a build, continuing the active request trace when present. */\nfunction enqueuerFor(queue: CaptureQueue): (buildId: string, reqId?: string) => Promise<void> {\n return async (buildId, reqId) => {\n const traceparent = currentTraceparent();\n await queue.enqueue({ buildId, reqId, ...(traceparent ? { traceparent } : {}) });\n };\n}\n\n/**\n * Wire capture for `options`: `testing` always has a runner, so this instance consumes the\n * captures it queues. Shard jobs go back onto the same queue the capture came from.\n */\nexport function setupCapture(\n options: ShelfOptions,\n config: ShelfConfig,\n delivery: Delivery,\n logger: Logger,\n jobs: JobQueue,\n): CaptureWiring {\n const testing = options.testing;\n if (testing === undefined) {\n return { queue: null, enqueueCapture: undefined, handler: null };\n }\n const queue = jobs.view;\n const enqueueCapture = enqueuerFor(queue);\n const handlerOptions = captureHandlerOptions(options, config, delivery, logger, {\n runner: testing.runner,\n enqueue: async (job) => {\n await queue.enqueue(job);\n },\n });\n const retries = options.testing?.retries ?? DEFAULT_RETRIES;\n const dispatch = captureDispatchDeps(handlerOptions, retries);\n const failed = async (job: ConsumerJob, error: string): Promise<void> => {\n await failCrashedCapture(dispatch, job, error);\n await notifyCaptureFailed(options, config, logger, job.buildId, delivery.publicOrigin);\n };\n jobs.exhausted.capture = failed;\n const inProcess = createCaptureHandler(handlerOptions);\n const handler = shouldForkCapture(testing, logger)\n ? forkedHandler(options, { fallback: inProcess, retries, onCrashed: failed, logger })\n : inProcess;\n return {\n queue,\n enqueueCapture,\n handler: alerting(handler, retries, async (buildId) => {\n await notifyCaptureFailed(options, config, logger, buildId, delivery.publicOrigin);\n }),\n };\n}\n\nfunction captureHandlerOptions(\n options: ShelfOptions,\n config: ShelfConfig,\n delivery: Delivery,\n logger: Logger,\n parts: { runner: CaptureRunner; enqueue: CaptureHandlerOptions[\"enqueue\"] },\n): CaptureHandlerOptions {\n return {\n db: options.database,\n storage: options.storage,\n runner: parts.runner,\n // `resolveShelfOptions` defaults scratchDir; the fallback covers hand-built configs.\n scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n viewports: config.viewports,\n logger,\n secret: credentialKeys(config),\n design: options.design,\n shardSize: options.testing?.shardSize,\n notifiers: delivery.notifiers,\n buildState: delivery.buildState,\n publicOrigin: delivery.publicOrigin,\n config,\n enqueue: parts.enqueue,\n maxRetries: options.testing?.retries,\n };\n}\n\nfunction forkedHandler(\n options: ShelfOptions,\n parts: {\n fallback: KindHandler;\n retries: number;\n onCrashed: (job: ConsumerJob, error: string) => Promise<void>;\n logger: Logger;\n },\n): CaptureHandler {\n const testing = options.testing;\n return createForkedHandler({\n workerEntry: testing?.workerEntry ?? \"\",\n shardSize: testing?.shardSize,\n imageEngine: testing?.images?.engine,\n diffThreads: testing?.compare?.threads,\n ...parts,\n });\n}\n\n/**\n * Alert admins when a capture left its build failed: after a run that ended, and after a final\n * attempt that threw (the build may then already be failed by a crash hook). Alerts stay in this\n * process whichever process rendered.\n */\nfunction alerting(\n inner: CaptureHandler,\n retries: number,\n alert: (buildId: string) => Promise<void>,\n): CaptureHandler {\n return {\n accepts: (job) => inner.accepts?.(job) !== false,\n run: async (job, context) => {\n try {\n await inner.run(job, context);\n } catch (error) {\n if (isFinalAttempt(job, retries)) {\n await alert(job.buildId);\n }\n throw error;\n }\n await alert(job.buildId);\n },\n cancel: async (job) => {\n await inner.cancel?.(job);\n },\n ...(inner.close ? { close: inner.close } : {}),\n };\n}\n"],"mappings":";;;;;;;;;;;AAqDA,SAAS,YAAY,OAAyE;CAC5F,OAAO,OAAO,SAAS,UAAU;EAC/B,MAAM,cAAc,mBAAmB;EACvC,MAAM,MAAM,QAAQ;GAAE;GAAS;GAAO,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EAAG,CAAC;CACjF;AACF;;;;;AAMA,SAAgB,aACd,SACA,QACA,UACA,QACA,MACe;CACf,MAAM,UAAU,QAAQ;CACxB,IAAI,YAAY,KAAA,GACd,OAAO;EAAE,OAAO;EAAM,gBAAgB,KAAA;EAAW,SAAS;CAAK;CAEjE,MAAM,QAAQ,KAAK;CACnB,MAAM,iBAAiB,YAAY,KAAK;CACxC,MAAM,iBAAiB,sBAAsB,SAAS,QAAQ,UAAU,QAAQ;EAC9E,QAAQ,QAAQ;EAChB,SAAS,OAAO,QAAQ;GACtB,MAAM,MAAM,QAAQ,GAAG;EACzB;CACF,CAAC;CACD,MAAM,UAAU,QAAQ,SAAS,WAAW;CAC5C,MAAM,WAAW,oBAAoB,gBAAgB,OAAO;CAC5D,MAAM,SAAS,OAAO,KAAkB,UAAiC;EACvE,MAAM,mBAAmB,UAAU,KAAK,KAAK;EAC7C,MAAM,oBAAoB,SAAS,QAAQ,QAAQ,IAAI,SAAS,SAAS,YAAY;CACvF;CACA,KAAK,UAAU,UAAU;CACzB,MAAM,YAAY,qBAAqB,cAAc;CAIrD,OAAO;EACL;EACA;EACA,SAAS,SANK,kBAAkB,SAAS,MAAM,IAC7C,cAAc,SAAS;GAAE,UAAU;GAAW;GAAS,WAAW;GAAQ;EAAO,CAAC,IAClF,WAIyB,SAAS,OAAO,YAAY;GACrD,MAAM,oBAAoB,SAAS,QAAQ,QAAQ,SAAS,SAAS,YAAY;EACnF,CAAC;CACH;AACF;AAEA,SAAS,sBACP,SACA,QACA,UACA,QACA,OACuB;CACvB,OAAO;EACL,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB,QAAQ,MAAM;EAEd,YAAY,OAAO,cAAc;EACjC,WAAW,OAAO;EAClB;EACA,QAAQ,eAAe,MAAM;EAC7B,QAAQ,QAAQ;EAChB,WAAW,QAAQ,SAAS;EAC5B,WAAW,SAAS;EACpB,YAAY,SAAS;EACrB,cAAc,SAAS;EACvB;EACA,SAAS,MAAM;EACf,YAAY,QAAQ,SAAS;CAC/B;AACF;AAEA,SAAS,cACP,SACA,OAMgB;CAChB,MAAM,UAAU,QAAQ;CACxB,OAAO,oBAAoB;EACzB,aAAa,SAAS,eAAe;EACrC,WAAW,SAAS;EACpB,aAAa,SAAS,QAAQ;EAC9B,aAAa,SAAS,SAAS;EAC/B,GAAG;CACL,CAAC;AACH;;;;;;AAOA,SAAS,SACP,OACA,SACA,OACgB;CAChB,OAAO;EACL,UAAU,QAAQ,MAAM,UAAU,GAAG,MAAM;EAC3C,KAAK,OAAO,KAAK,YAAY;GAC3B,IAAI;IACF,MAAM,MAAM,IAAI,KAAK,OAAO;GAC9B,SAAS,OAAO;IACd,IAAI,eAAe,KAAK,OAAO,GAC7B,MAAM,MAAM,IAAI,OAAO;IAEzB,MAAM;GACR;GACA,MAAM,MAAM,IAAI,OAAO;EACzB;EACA,QAAQ,OAAO,QAAQ;GACrB,MAAM,MAAM,SAAS,GAAG;EAC1B;EACA,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;CAC9C;AACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "./credential-keys.mjs";
|
|
5
|
+
import { getStore } from "./store.mjs";
|
|
6
|
+
import { ConnectionModel } from "@storyshelf/core/models";
|
|
7
|
+
//#region src/connections.ts
|
|
8
|
+
/**
|
|
9
|
+
* Services a user can connect their own account to. Today only design providers
|
|
10
|
+
* offer OAuth; a future issue-tracker or notifier adapter that exposes
|
|
11
|
+
* `oauth` joins by being listed here — routes, state signing, storage and
|
|
12
|
+
* refresh are shared.
|
|
13
|
+
*/
|
|
14
|
+
function connectableServices() {
|
|
15
|
+
return [...getStore().design];
|
|
16
|
+
}
|
|
17
|
+
/** The service addressed by `kind` when the operator configured OAuth for it, else undefined. */
|
|
18
|
+
function findConnectable(kind) {
|
|
19
|
+
return connectableServices().find((service) => service.metadata.kind === kind && service.oauth);
|
|
20
|
+
}
|
|
21
|
+
/** Connection storage bound to the server's credential keys. */
|
|
22
|
+
function connectionModel() {
|
|
23
|
+
const { db, config } = getStore();
|
|
24
|
+
return new ConnectionModel(db, credentialKeys(config));
|
|
25
|
+
}
|
|
26
|
+
/** Connecting needs authentication (a user to own it) and a server secret (to sign the flow). */
|
|
27
|
+
function canConnect() {
|
|
28
|
+
const { authEnabled, user, config } = getStore();
|
|
29
|
+
return authEnabled && Boolean(user) && Boolean(config.secret);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { canConnect, connectableServices, connectionModel, findConnectable };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=connections.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.mjs","names":[],"sources":["../src/connections.ts"],"sourcesContent":["import type { OAuthConnectable } from \"@storyshelf/core/adapter/oauth\";\nimport { ConnectionModel } from \"@storyshelf/core/models\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { getStore } from \"./store.ts\";\n\n/**\n * Services a user can connect their own account to. Today only design providers\n * offer OAuth; a future issue-tracker or notifier adapter that exposes\n * `oauth` joins by being listed here — routes, state signing, storage and\n * refresh are shared.\n */\nexport function connectableServices(): OAuthConnectable[] {\n return [...getStore().design];\n}\n\n/** A connectable service whose operator configured OAuth. */\nexport type Connectable = Required<OAuthConnectable>;\n\n/** The service addressed by `kind` when the operator configured OAuth for it, else undefined. */\nexport function findConnectable(kind: string): Connectable | undefined {\n const found = connectableServices().find(\n (service) => service.metadata.kind === kind && service.oauth,\n );\n return found as Connectable | undefined;\n}\n\n/** Connection storage bound to the server's credential keys. */\nexport function connectionModel(): ConnectionModel {\n const { db, config } = getStore();\n return new ConnectionModel(db, credentialKeys(config));\n}\n\n/** Connecting needs authentication (a user to own it) and a server secret (to sign the flow). */\nexport function canConnect(): boolean {\n const { authEnabled, user, config } = getStore();\n return authEnabled && Boolean(user) && Boolean(config.secret);\n}\n"],"mappings":";;;;;;;;;;;;;AAWA,SAAgB,sBAA0C;CACxD,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,MAAM;AAC9B;;AAMA,SAAgB,gBAAgB,MAAuC;CAIrE,OAHc,oBAAoB,CAAC,CAAC,MACjC,YAAY,QAAQ,SAAS,SAAS,QAAQ,QAAQ,KAE9C;AACb;;AAGA,SAAgB,kBAAmC;CACjD,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,gBAAgB,IAAI,eAAe,MAAM,CAAC;AACvD;;AAGA,SAAgB,aAAsB;CACpC,MAAM,EAAE,aAAa,MAAM,WAAW,SAAS;CAC/C,OAAO,eAAe,QAAQ,IAAI,KAAK,QAAQ,OAAO,MAAM;AAC9D"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { captureMetrics } from "@storyshelf/observability";
|
|
5
|
+
import { createCaptureConsumer } from "@storyshelf/core/capture";
|
|
6
|
+
//#region src/consumer-setup.ts
|
|
7
|
+
/**
|
|
8
|
+
* The consumer of the app's queues (ADR 0006, decision 5): built when first started, so a host
|
|
9
|
+
* that never starts one never touches the queue, and drained as a unit on teardown.
|
|
10
|
+
*/
|
|
11
|
+
/** One consumer from options that are only worked out on the first `start()`. */
|
|
12
|
+
function lazyConsumer(options, closers = []) {
|
|
13
|
+
let built;
|
|
14
|
+
return {
|
|
15
|
+
start: async () => {
|
|
16
|
+
built ??= createCaptureConsumer(options());
|
|
17
|
+
await built.start();
|
|
18
|
+
},
|
|
19
|
+
stop: async () => {
|
|
20
|
+
await built?.stop();
|
|
21
|
+
for (const close of closers) await close();
|
|
22
|
+
},
|
|
23
|
+
isRunning: () => built?.isRunning() ?? false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Several consumers as one: they start together and drain together. */
|
|
27
|
+
function combineConsumers(consumers) {
|
|
28
|
+
const [only, ...rest] = consumers;
|
|
29
|
+
if (only === void 0) return null;
|
|
30
|
+
if (rest.length === 0) return only;
|
|
31
|
+
return {
|
|
32
|
+
start: async () => {
|
|
33
|
+
await Promise.all(consumers.map(async (consumer) => {
|
|
34
|
+
await consumer.start();
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
stop: async () => {
|
|
38
|
+
await Promise.all(consumers.map(async (consumer) => {
|
|
39
|
+
await consumer.stop();
|
|
40
|
+
}));
|
|
41
|
+
},
|
|
42
|
+
isRunning: () => consumers.some((consumer) => consumer.isRunning())
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Job counts and durations on the capture instruments, told apart by `job.kind`. */
|
|
46
|
+
function jobMetrics() {
|
|
47
|
+
return { jobFinished: (event) => {
|
|
48
|
+
const instruments = captureMetrics();
|
|
49
|
+
const attributes = { "job.kind": event.kind };
|
|
50
|
+
instruments.jobDuration.record(event.durationMs, attributes);
|
|
51
|
+
if (event.outcome === "completed") instruments.jobsCompleted.add(1, attributes);
|
|
52
|
+
else if (event.outcome === "failed") instruments.jobsFailed.add(1, attributes);
|
|
53
|
+
} };
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { combineConsumers, jobMetrics, lazyConsumer };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=consumer-setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer-setup.mjs","names":[],"sources":["../src/consumer-setup.ts"],"sourcesContent":["/**\n * The consumer of the app's queues (ADR 0006, decision 5): built when first started, so a host\n * that never starts one never touches the queue, and drained as a unit on teardown.\n */\nimport {\n createCaptureConsumer,\n type CaptureConsumer,\n type CaptureConsumerOptions,\n type ConsumerMetrics,\n} from \"@storyshelf/core/capture\";\nimport { captureMetrics } from \"@storyshelf/observability\";\n\n/** One consumer from options that are only worked out on the first `start()`. */\nexport function lazyConsumer(\n options: () => CaptureConsumerOptions,\n closers: (() => Promise<void>)[] = [],\n): CaptureConsumer {\n let built: CaptureConsumer | undefined;\n return {\n start: async () => {\n built ??= createCaptureConsumer(options());\n await built.start();\n },\n // Stopping one never started is a no-op; the closers (forked child, queue wake) still run.\n stop: async () => {\n await built?.stop();\n for (const close of closers) {\n // oxlint-disable-next-line no-await-in-loop -- close in order, after the drain\n await close();\n }\n },\n isRunning: () => built?.isRunning() ?? false,\n };\n}\n\n/** Several consumers as one: they start together and drain together. */\nexport function combineConsumers(consumers: CaptureConsumer[]): CaptureConsumer | null {\n const [only, ...rest] = consumers;\n if (only === undefined) {\n return null;\n }\n if (rest.length === 0) {\n return only;\n }\n return {\n start: async () => {\n await Promise.all(\n consumers.map(async (consumer) => {\n await consumer.start();\n }),\n );\n },\n stop: async () => {\n await Promise.all(\n consumers.map(async (consumer) => {\n await consumer.stop();\n }),\n );\n },\n isRunning: () => consumers.some((consumer) => consumer.isRunning()),\n };\n}\n\n/** Job counts and durations on the capture instruments, told apart by `job.kind`. */\nexport function jobMetrics(): ConsumerMetrics {\n return {\n jobFinished: (event) => {\n const instruments = captureMetrics();\n const attributes = { \"job.kind\": event.kind };\n instruments.jobDuration.record(event.durationMs, attributes);\n if (event.outcome === \"completed\") {\n instruments.jobsCompleted.add(1, attributes);\n } else if (event.outcome === \"failed\") {\n instruments.jobsFailed.add(1, attributes);\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,aACd,SACA,UAAmC,CAAC,GACnB;CACjB,IAAI;CACJ,OAAO;EACL,OAAO,YAAY;GACjB,UAAU,sBAAsB,QAAQ,CAAC;GACzC,MAAM,MAAM,MAAM;EACpB;EAEA,MAAM,YAAY;GAChB,MAAM,OAAO,KAAK;GAClB,KAAK,MAAM,SAAS,SAElB,MAAM,MAAM;EAEhB;EACA,iBAAiB,OAAO,UAAU,KAAK;CACzC;AACF;;AAGA,SAAgB,iBAAiB,WAAsD;CACrF,MAAM,CAAC,MAAM,GAAG,QAAQ;CACxB,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI,KAAK,WAAW,GAClB,OAAO;CAET,OAAO;EACL,OAAO,YAAY;GACjB,MAAM,QAAQ,IACZ,UAAU,IAAI,OAAO,aAAa;IAChC,MAAM,SAAS,MAAM;GACvB,CAAC,CACH;EACF;EACA,MAAM,YAAY;GAChB,MAAM,QAAQ,IACZ,UAAU,IAAI,OAAO,aAAa;IAChC,MAAM,SAAS,KAAK;GACtB,CAAC,CACH;EACF;EACA,iBAAiB,UAAU,MAAM,aAAa,SAAS,UAAU,CAAC;CACpE;AACF;;AAGA,SAAgB,aAA8B;CAC5C,OAAO,EACL,cAAc,UAAU;EACtB,MAAM,cAAc,eAAe;EACnC,MAAM,aAAa,EAAE,YAAY,MAAM,KAAK;EAC5C,YAAY,YAAY,OAAO,MAAM,YAAY,UAAU;EAC3D,IAAI,MAAM,YAAY,aACpB,YAAY,cAAc,IAAI,GAAG,UAAU;OACtC,IAAI,MAAM,YAAY,UAC3B,YAAY,WAAW,IAAI,GAAG,UAAU;CAE5C,EACF;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "./credential-keys.mjs";
|
|
5
|
+
import { getStore } from "./store.mjs";
|
|
6
|
+
import { notificationOrigin } from "./notifications-pause.mjs";
|
|
7
|
+
import { buildStateFlags } from "./build-state-flags.mjs";
|
|
8
|
+
import { createBuildStatePublisher, requestOpenBuildStates } from "@storyshelf/core/capture";
|
|
9
|
+
//#region src/git-status.ts
|
|
10
|
+
/** Ask for a build's state to be shown on its project's git channels (ADR 0014, decision 5). */
|
|
11
|
+
/**
|
|
12
|
+
* Show the build's current state (commit statuses and the pull-request comment) on every git
|
|
13
|
+
* channel of its project. Called on every transition a request causes: build creation, a review
|
|
14
|
+
* outcome, a retry. Delivery reads the build when it runs, so the call needs no state of its own;
|
|
15
|
+
* it never rejects, so a git host cannot fail the action that triggered it.
|
|
16
|
+
*/
|
|
17
|
+
async function requestBuildState(buildId) {
|
|
18
|
+
const store = getStore();
|
|
19
|
+
await (store.buildState ?? inlinePublisher(store)).request(buildId);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An origin just became resolved (an admin confirmed or reported one): requests made while none was
|
|
23
|
+
* were dropped, so ask again for the builds that can still change (ADR 0014, decision 11).
|
|
24
|
+
* Resolves to how many builds were asked; never rejects.
|
|
25
|
+
*/
|
|
26
|
+
async function requestOpenBuildStatesNow() {
|
|
27
|
+
const store = getStore();
|
|
28
|
+
return await requestOpenBuildStates(store, { request: requestBuildState });
|
|
29
|
+
}
|
|
30
|
+
/** Hand-built stores have no wiring: deliver inline through the store's own adapters. */
|
|
31
|
+
function inlinePublisher(store) {
|
|
32
|
+
return createBuildStatePublisher({ deps: {
|
|
33
|
+
db: store.db,
|
|
34
|
+
storage: store.storage,
|
|
35
|
+
notifiers: store.notifiers,
|
|
36
|
+
secret: credentialKeys(store.config),
|
|
37
|
+
config: store.config,
|
|
38
|
+
publicOrigin: async () => await notificationOrigin(store),
|
|
39
|
+
flags: (project) => buildStateFlags(store.testing, project),
|
|
40
|
+
logger: store.logger
|
|
41
|
+
} });
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { requestBuildState, requestOpenBuildStatesNow };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=git-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-status.mjs","names":[],"sources":["../src/git-status.ts"],"sourcesContent":["/** Ask for a build's state to be shown on its project's git channels (ADR 0014, decision 5). */\nimport { createBuildStatePublisher, requestOpenBuildStates } from \"@storyshelf/core/capture\";\nimport { buildStateFlags } from \"./build-state-flags.ts\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { notificationOrigin } from \"./notifications-pause.ts\";\nimport { getStore, type Store } from \"./store.ts\";\n\n/**\n * Show the build's current state (commit statuses and the pull-request comment) on every git\n * channel of its project. Called on every transition a request causes: build creation, a review\n * outcome, a retry. Delivery reads the build when it runs, so the call needs no state of its own;\n * it never rejects, so a git host cannot fail the action that triggered it.\n */\nexport async function requestBuildState(buildId: string): Promise<void> {\n const store = getStore();\n await (store.buildState ?? inlinePublisher(store)).request(buildId);\n}\n\n/**\n * An origin just became resolved (an admin confirmed or reported one): requests made while none was\n * were dropped, so ask again for the builds that can still change (ADR 0014, decision 11).\n * Resolves to how many builds were asked; never rejects.\n */\nexport async function requestOpenBuildStatesNow(): Promise<number> {\n const store = getStore();\n return await requestOpenBuildStates(store, { request: requestBuildState });\n}\n\n/** Hand-built stores have no wiring: deliver inline through the store's own adapters. */\nfunction inlinePublisher(store: Store): ReturnType<typeof createBuildStatePublisher> {\n return createBuildStatePublisher({\n deps: {\n db: store.db,\n storage: store.storage,\n notifiers: store.notifiers,\n secret: credentialKeys(store.config),\n config: store.config,\n publicOrigin: async () => await notificationOrigin(store),\n flags: (project) => buildStateFlags(store.testing, project),\n logger: store.logger,\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAaA,eAAsB,kBAAkB,SAAgC;CACtE,MAAM,QAAQ,SAAS;CACvB,OAAO,MAAM,cAAc,gBAAgB,KAAK,EAAA,CAAG,QAAQ,OAAO;AACpE;;;;;;AAOA,eAAsB,4BAA6C;CACjE,MAAM,QAAQ,SAAS;CACvB,OAAO,MAAM,uBAAuB,OAAO,EAAE,SAAS,kBAAkB,CAAC;AAC3E;;AAGA,SAAS,gBAAgB,OAA4D;CACnF,OAAO,0BAA0B,EAC/B,MAAM;EACJ,IAAI,MAAM;EACV,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,QAAQ,eAAe,MAAM,MAAM;EACnC,QAAQ,MAAM;EACd,cAAc,YAAY,MAAM,mBAAmB,KAAK;EACxD,QAAQ,YAAY,gBAAgB,MAAM,SAAS,OAAO;EAC1D,QAAQ,MAAM;CAChB,EACF,CAAC;AACH"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/hidden-data.ts
|
|
5
|
+
/**
|
|
6
|
+
* Snapshots stored in the database. With `testing` off they are hidden, not deleted (ADR 0006,
|
|
7
|
+
* decision 1): the System page and the startup log say how many.
|
|
8
|
+
*/
|
|
9
|
+
async function countHiddenSnapshots(db) {
|
|
10
|
+
return await db.count(db.tables.snapshots);
|
|
11
|
+
}
|
|
12
|
+
/** Log the mode at boot; with `testing` off, also how much captured data it hides. */
|
|
13
|
+
async function logHostingOnlyMode(db, logger) {
|
|
14
|
+
const hiddenSnapshots = await countHiddenSnapshots(db);
|
|
15
|
+
if (hiddenSnapshots > 0) {
|
|
16
|
+
logger.warn({ hiddenSnapshots }, "testing is off: existing snapshots are hidden, not deleted; they reappear when testing is turned on");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
logger.info("testing is off: hosting Storybooks only (no capture, compare or baselines)");
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { countHiddenSnapshots, logHostingOnlyMode };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=hidden-data.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hidden-data.mjs","names":[],"sources":["../src/hidden-data.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n\n/**\n * Snapshots stored in the database. With `testing` off they are hidden, not deleted (ADR 0006,\n * decision 1): the System page and the startup log say how many.\n */\nexport async function countHiddenSnapshots(db: DatabaseAdapter): Promise<number> {\n return await db.count(db.tables.snapshots);\n}\n\n/** Log the mode at boot; with `testing` off, also how much captured data it hides. */\nexport async function logHostingOnlyMode(db: DatabaseAdapter, logger: Logger): Promise<void> {\n const hiddenSnapshots = await countHiddenSnapshots(db);\n if (hiddenSnapshots > 0) {\n logger.warn(\n { hiddenSnapshots },\n \"testing is off: existing snapshots are hidden, not deleted; they reappear when testing is turned on\",\n );\n return;\n }\n logger.info(\"testing is off: hosting Storybooks only (no capture, compare or baselines)\");\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,qBAAqB,IAAsC;CAC/E,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,SAAS;AAC3C;;AAGA,eAAsB,mBAAmB,IAAqB,QAA+B;CAC3F,MAAM,kBAAkB,MAAM,qBAAqB,EAAE;CACrD,IAAI,kBAAkB,GAAG;EACvB,OAAO,KACL,EAAE,gBAAgB,GAClB,qGACF;EACA;CACF;CACA,OAAO,KAAK,4EAA4E;AAC1F"}
|