@bugmole/cli 0.4.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/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// GitLab merge request checks: unlike GitHub, there is no Bugmole-operated
|
|
2
|
+
// app to install. Each binding brings its own project access token (scope
|
|
3
|
+
// `api`) and a per-binding webhook secret, generated when the binding is
|
|
4
|
+
// created and shown once so it can be pasted into the project's webhook
|
|
5
|
+
// settings. The webhook posts straight to the registry — there's no shared
|
|
6
|
+
// operator secret to verify, so no dashboard hop is needed the way GitHub's
|
|
7
|
+
// is.
|
|
8
|
+
import { fetchGitlabProject, GitlabApiError, normalizeGitlabHost, postCommitStatus, validGitlabHost, type GitlabFetchEnv } from "../../integrations/gitlab.js";
|
|
9
|
+
import type { Env, Principal } from "../core.js";
|
|
10
|
+
import { constantTimeEqual, memberOfProject, newId, randomToken, requestJson, response } from "../core.js";
|
|
11
|
+
import { runChangedHooks, runService, type InternalRunFields } from "../hooks.js";
|
|
12
|
+
import { auditProject } from "../org/audit.js";
|
|
13
|
+
import { enqueue, PermanentDeliveryError, registerOutboxHandler } from "../notifications/outbox.js";
|
|
14
|
+
import { openSecret, sealSecret } from "../notifications/secrets.js";
|
|
15
|
+
import { previewUrl } from "../repo-triggers.js";
|
|
16
|
+
import { detachedContext, matchPath, registerPublicRoutes, registerRoutes, type AuthedRouteContext } from "../routes/index.js";
|
|
17
|
+
import { prChecksOn } from "../github/checks.js";
|
|
18
|
+
import { digging, molesFound } from "../../runtime/mole-voice.js";
|
|
19
|
+
|
|
20
|
+
const TRIGGERS = new Set(["merge_request"]);
|
|
21
|
+
const PROJECT_PATH = /^[A-Za-z0-9_.-]{1,100}(?:\/[A-Za-z0-9_.-]{1,100})+$/;
|
|
22
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
23
|
+
|
|
24
|
+
type Binding = {
|
|
25
|
+
id: string;
|
|
26
|
+
project_id: string;
|
|
27
|
+
gitlab_host: string;
|
|
28
|
+
gitlab_project_id: number;
|
|
29
|
+
gitlab_project_path: string;
|
|
30
|
+
access_token_ciphertext: string;
|
|
31
|
+
access_token_iv: string;
|
|
32
|
+
webhook_secret_ciphertext: string;
|
|
33
|
+
webhook_secret_iv: string;
|
|
34
|
+
plan_ids_json: string | null;
|
|
35
|
+
environment_id: string | null;
|
|
36
|
+
trigger: "merge_request";
|
|
37
|
+
base_url_template: string | null;
|
|
38
|
+
execution_mode: "self" | "cloud";
|
|
39
|
+
created_by: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function gitlabEnv(env: Env): GitlabFetchEnv {
|
|
43
|
+
return env as Env & GitlabFetchEnv;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dashboard(env: Env): string {
|
|
47
|
+
return (env.DASHBOARD_URL || "https://app.bugmole.com").replace(/\/$/, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function webhookUrl(env: Env, bindingId: string): string {
|
|
51
|
+
return `${(env.REGISTRY_PUBLIC_URL || "https://api.bugmole.com").replace(/\/$/, "")}/api/gitlab/webhook?binding=${bindingId}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function publishedPlans(env: Env, projectId: string, only: string[] | null): Promise<string[]> {
|
|
55
|
+
const rows = await env.DB.prepare("SELECT plan_id AS planId FROM plan_drafts WHERE project_id = ? ORDER BY plan_id").bind(projectId).all<{ planId: string }>();
|
|
56
|
+
const all = rows.results.map((row) => row.planId);
|
|
57
|
+
return only ? all.filter((planId) => only.includes(planId)) : all;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function startRun(env: Env, binding: Binding, planId: string, idempotencyKey: string, internal: InternalRunFields): Promise<string | null> {
|
|
61
|
+
if (!runService.create) throw new Error("Run creation isn't wired");
|
|
62
|
+
const principal: Principal = { aegisId: binding.created_by, projectIds: [], scopes: ["*"] };
|
|
63
|
+
const request = new Request("https://registry.internal/gitlab", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: JSON.stringify({ planId, idempotencyKey, executionMode: binding.execution_mode, ...(binding.environment_id ? { environmentId: binding.environment_id } : {}) }),
|
|
66
|
+
});
|
|
67
|
+
const created = await runService.create(request, env, principal, binding.project_id, detachedContext(), internal);
|
|
68
|
+
const body = await created.json().catch(() => ({})) as { run?: { id: string }; error?: string };
|
|
69
|
+
if (!created.ok || !body.run) {
|
|
70
|
+
console.warn(`[gitlab] couldn't start ${planId} for ${binding.gitlab_project_path}: ${body.error ?? created.status}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// A new run (201) gets an initial status; a repeat delivery (200) already has one.
|
|
74
|
+
if (created.status === 201) await enqueue(env, "gitlab.status.update", { runId: body.run.id }, { dedupeKey: `gl-status:${body.run.id}:pending` });
|
|
75
|
+
return body.run.id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Merge request events ---------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export type GitlabMergeRequestEvent = {
|
|
81
|
+
gitlabProjectId: number;
|
|
82
|
+
action: string;
|
|
83
|
+
hasCommit: boolean;
|
|
84
|
+
iid: number;
|
|
85
|
+
sha: string;
|
|
86
|
+
branch?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** GitLab's "Merge Request Hook" payload; only the fields Bugmole acts on. */
|
|
90
|
+
export function parseMergeRequestEvent(body: Record<string, unknown> | null): GitlabMergeRequestEvent | null {
|
|
91
|
+
if (!body || body.object_kind !== "merge_request") return null;
|
|
92
|
+
const project = body.project as Record<string, unknown> | undefined;
|
|
93
|
+
const attrs = body.object_attributes as Record<string, unknown> | undefined;
|
|
94
|
+
const gitlabProjectId = Number(project?.id);
|
|
95
|
+
if (!Number.isInteger(gitlabProjectId) || gitlabProjectId <= 0) return null;
|
|
96
|
+
const action = typeof attrs?.action === "string" ? attrs.action : "";
|
|
97
|
+
const iid = Number(attrs?.iid);
|
|
98
|
+
if (!Number.isInteger(iid) || iid <= 0) return null;
|
|
99
|
+
const lastCommit = attrs?.last_commit as Record<string, unknown> | undefined;
|
|
100
|
+
const sha = typeof lastCommit?.id === "string" ? lastCommit.id.toLowerCase() : "";
|
|
101
|
+
if (!SHA.test(sha)) return null;
|
|
102
|
+
const branch = typeof attrs?.source_branch === "string" ? attrs.source_branch : undefined;
|
|
103
|
+
// "update" fires for label/description edits too; `oldrev` is only present when a commit was pushed.
|
|
104
|
+
return { gitlabProjectId, action, hasCommit: attrs?.oldrev !== undefined, iid, sha, branch };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldRun(event: GitlabMergeRequestEvent): boolean {
|
|
108
|
+
return event.action === "open" || event.action === "reopen" || (event.action === "update" && event.hasCommit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function handleMergeRequestEvent(env: Env, binding: Binding, event: GitlabMergeRequestEvent): Promise<{ started: string[] }> {
|
|
112
|
+
if (!shouldRun(event) || !(await prChecksOn(env, binding.project_id))) return { started: [] };
|
|
113
|
+
let baseUrl: string | undefined;
|
|
114
|
+
if (binding.base_url_template) {
|
|
115
|
+
const url = previewUrl(binding.base_url_template, { number: event.iid, sha: event.sha, branch: event.branch });
|
|
116
|
+
if (!url) return { started: [] };
|
|
117
|
+
baseUrl = url;
|
|
118
|
+
}
|
|
119
|
+
const started: string[] = [];
|
|
120
|
+
const plans = await publishedPlans(env, binding.project_id, binding.plan_ids_json ? JSON.parse(binding.plan_ids_json) as string[] : null);
|
|
121
|
+
for (const planId of plans) {
|
|
122
|
+
const id = await startRun(env, binding, planId, `gl:${binding.id}:${event.sha}:${planId}`, {
|
|
123
|
+
triggerSource: "merge_request",
|
|
124
|
+
repoFullName: binding.gitlab_project_path,
|
|
125
|
+
commitSha: event.sha,
|
|
126
|
+
prNumber: event.iid,
|
|
127
|
+
gitlabBindingId: binding.id,
|
|
128
|
+
environmentBaseUrl: baseUrl,
|
|
129
|
+
});
|
|
130
|
+
if (id) started.push(id);
|
|
131
|
+
}
|
|
132
|
+
return { started };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Commit statuses ---------------------------------------------------------------------------
|
|
136
|
+
//
|
|
137
|
+
// GitLab has no persistent check-run id to create then update: posting a new
|
|
138
|
+
// status with the same `name` for the same commit replaces the previous one.
|
|
139
|
+
|
|
140
|
+
type RunForStatus = {
|
|
141
|
+
id: string; projectId: string; planId: string; status: string; sha: string | null; bindingId: string | null;
|
|
142
|
+
passed: number | null; failed: number | null; baseUrl: string | null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
async function runForStatus(env: Env, runId: string): Promise<RunForStatus | null> {
|
|
146
|
+
return env.DB.prepare(`
|
|
147
|
+
SELECT id, project_id AS projectId, plan_id AS planId, status, commit_sha AS sha, gitlab_binding_id AS bindingId,
|
|
148
|
+
steps_passed AS passed, steps_failed AS failed, environment_base_url AS baseUrl
|
|
149
|
+
FROM runs WHERE id = ?
|
|
150
|
+
`).bind(runId).first<RunForStatus>();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function statusFor(run: RunForStatus): { state: "pending" | "running" | "success" | "failed" | "canceled"; description: string } {
|
|
154
|
+
if (run.status === "queued") return { state: "pending", description: "Waiting for a runner." };
|
|
155
|
+
if (run.status === "running") return { state: "running", description: digging(run.baseUrl) };
|
|
156
|
+
if (run.status === "cancelled") return { state: "canceled", description: "Cancelled." };
|
|
157
|
+
const state = run.status === "success" ? "success" : "failed";
|
|
158
|
+
const on = run.baseUrl ? ` on ${run.baseUrl}` : "";
|
|
159
|
+
const found = state === "success" || (run.failed ?? 0) > 0 ? `${molesFound(state === "success" ? 0 : run.failed ?? 0)}: ` : "";
|
|
160
|
+
return { state, description: `${found}${run.passed ?? 0} step(s) passed, ${run.failed ?? 0} failed${on}.` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function deliver(work: () => Promise<unknown>): Promise<void> {
|
|
164
|
+
try {
|
|
165
|
+
await work();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// A revoked token or removed project (401/403/404/422) won't start working on a retry.
|
|
168
|
+
if (error instanceof GitlabApiError && [401, 403, 404, 422].includes(error.status)) throw new PermanentDeliveryError(error.message);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
registerOutboxHandler("gitlab.status.update", async (env, payload: { runId: string }) => {
|
|
174
|
+
const run = await runForStatus(env, payload.runId);
|
|
175
|
+
if (!run?.sha || !run.bindingId) return;
|
|
176
|
+
const binding = await env.DB.prepare("SELECT * FROM project_gitlab_bindings WHERE id = ?").bind(run.bindingId).first<Binding>();
|
|
177
|
+
if (!binding) return;
|
|
178
|
+
const token = await openSecret(env, binding.access_token_ciphertext, binding.access_token_iv);
|
|
179
|
+
const { state, description } = statusFor(run);
|
|
180
|
+
await deliver(() => postCommitStatus(gitlabEnv(env), binding.gitlab_host, token, binding.gitlab_project_id, run.sha!, {
|
|
181
|
+
state,
|
|
182
|
+
name: `Bugmole: ${run.planId}`,
|
|
183
|
+
description,
|
|
184
|
+
target_url: `${dashboard(env)}/runs?${new URLSearchParams({ project: run.projectId, run: run.id })}`,
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
runChangedHooks.push(async (env, event) => {
|
|
189
|
+
const run = await env.DB.prepare("SELECT gitlab_binding_id AS bindingId FROM runs WHERE id = ?").bind(event.runId).first<{ bindingId: string | null }>();
|
|
190
|
+
if (!run?.bindingId) return;
|
|
191
|
+
// One update per state; the handler always sends the run's latest state.
|
|
192
|
+
await enqueue(env, "gitlab.status.update", { runId: event.runId }, { dedupeKey: `gl-status:${event.runId}:${event.status}` });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- Routes ----------------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function validProjectPath(path: string): boolean {
|
|
198
|
+
return PROJECT_PATH.test(path) && !path.split("/").some((part) => part === "." || part === "..");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function listBindings({ env, principal }: AuthedRouteContext, projectId: string): Promise<Response> {
|
|
202
|
+
if (!(await memberOfProject(env, principal, projectId))) return response({ error: "Project scope is not allowed" }, 403);
|
|
203
|
+
const rows = await env.DB.prepare(`
|
|
204
|
+
SELECT id, gitlab_host AS gitlabHost, gitlab_project_id AS gitlabProjectId, gitlab_project_path AS gitlabProjectPath,
|
|
205
|
+
plan_ids_json AS planIds, environment_id AS environmentId, trigger, base_url_template AS baseUrlTemplate,
|
|
206
|
+
execution_mode AS executionMode, enabled, created_at AS createdAt
|
|
207
|
+
FROM project_gitlab_bindings WHERE project_id = ? ORDER BY created_at ASC
|
|
208
|
+
`).bind(projectId).all<Record<string, unknown> & { planIds: string | null; enabled: number }>();
|
|
209
|
+
return response({
|
|
210
|
+
available: await prChecksOn(env, projectId),
|
|
211
|
+
bindings: rows.results.map((row) => ({
|
|
212
|
+
...row,
|
|
213
|
+
planIds: row.planIds ? JSON.parse(row.planIds) : null,
|
|
214
|
+
enabled: Boolean(row.enabled),
|
|
215
|
+
webhookUrl: webhookUrl(env, String(row.id)),
|
|
216
|
+
})),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function saveBinding({ env, principal, request }: AuthedRouteContext, projectId: string, bindingId?: string): Promise<Response> {
|
|
221
|
+
if (!(await memberOfProject(env, principal, projectId, "project.manage"))) return response({ error: "Project scope is not allowed" }, 403);
|
|
222
|
+
if (request.method === "DELETE") {
|
|
223
|
+
const deleted = await env.DB.prepare("DELETE FROM project_gitlab_bindings WHERE id = ? AND project_id = ?").bind(bindingId, projectId).run();
|
|
224
|
+
if ((deleted.meta?.changes ?? 0) === 0) return response({ error: "Not found" }, 404);
|
|
225
|
+
await auditProject(env, principal, projectId, { action: "gitlab.checks_removed", targetType: "gitlab_binding", targetId: bindingId }, request);
|
|
226
|
+
return response({ deleted: true });
|
|
227
|
+
}
|
|
228
|
+
if (!(await prChecksOn(env, projectId))) {
|
|
229
|
+
return response({ error: "Merge request checks are included in Team and Business.", code: "plan_feature", feature: "prChecks", upgradeUrl: "/billing" }, 402);
|
|
230
|
+
}
|
|
231
|
+
const body = await requestJson(request);
|
|
232
|
+
const host = normalizeGitlabHost(typeof body?.host === "string" ? body.host : undefined);
|
|
233
|
+
const projectPath = typeof body?.projectPath === "string" ? body.projectPath.trim().replace(/^\/+|\/+$/g, "") : "";
|
|
234
|
+
const accessToken = typeof body?.accessToken === "string" ? body.accessToken.trim() : "";
|
|
235
|
+
const trigger = body?.trigger ?? "merge_request";
|
|
236
|
+
const planIds = body?.planIds === null || body?.planIds === undefined ? null : body.planIds;
|
|
237
|
+
const template = typeof body?.baseUrlTemplate === "string" && body.baseUrlTemplate.trim() ? body.baseUrlTemplate.trim() : null;
|
|
238
|
+
const executionMode = body?.executionMode ?? "cloud";
|
|
239
|
+
const environmentId = typeof body?.environmentId === "string" && body.environmentId ? body.environmentId : null;
|
|
240
|
+
const enabled = body?.enabled === undefined ? true : body.enabled === true;
|
|
241
|
+
if (!validGitlabHost(host) || !validProjectPath(projectPath)) return response({ error: "host and projectPath (group/project) are required" }, 422);
|
|
242
|
+
if (typeof trigger !== "string" || !TRIGGERS.has(trigger)) return response({ error: "trigger must be merge_request" }, 422);
|
|
243
|
+
if (executionMode !== "cloud" && executionMode !== "self") return response({ error: "executionMode must be cloud or self" }, 422);
|
|
244
|
+
if (planIds !== null && (!Array.isArray(planIds) || !planIds.every((entry) => typeof entry === "string"))) {
|
|
245
|
+
return response({ error: "planIds must be a list of plan ids, or null for all" }, 422);
|
|
246
|
+
}
|
|
247
|
+
if (template && !previewUrl(template, { number: 1, sha: "0".repeat(40), branch: "main" })) {
|
|
248
|
+
return response({ error: "The preview URL template must be an http(s) address; it can use {number}, {sha}, {short_sha} and {branch}" }, 422);
|
|
249
|
+
}
|
|
250
|
+
if (environmentId) {
|
|
251
|
+
const environment = await env.DB.prepare("SELECT id FROM project_environments WHERE project_id = ? AND id = ?").bind(projectId, environmentId).first();
|
|
252
|
+
if (!environment) return response({ error: "Environment not found in this project" }, 422);
|
|
253
|
+
}
|
|
254
|
+
if (!accessToken && !bindingId) return response({ error: "accessToken is required to connect a new project" }, 422);
|
|
255
|
+
let gitlabProjectId: number;
|
|
256
|
+
let gitlabProjectPath = projectPath;
|
|
257
|
+
let sealedToken: { ciphertext: string; iv: string } | null = null;
|
|
258
|
+
if (accessToken) {
|
|
259
|
+
let project;
|
|
260
|
+
try {
|
|
261
|
+
project = await fetchGitlabProject(gitlabEnv(env), host, accessToken, projectPath);
|
|
262
|
+
} catch {
|
|
263
|
+
return response({ error: `Couldn't reach ${projectPath} on ${host} with that token. Check the project path and that the token has the "api" scope.` }, 422);
|
|
264
|
+
}
|
|
265
|
+
gitlabProjectId = project.id;
|
|
266
|
+
gitlabProjectPath = project.path_with_namespace;
|
|
267
|
+
sealedToken = await sealSecret(env, accessToken);
|
|
268
|
+
} else {
|
|
269
|
+
const current = await env.DB.prepare("SELECT gitlab_project_id AS id, gitlab_project_path AS path FROM project_gitlab_bindings WHERE id = ? AND project_id = ?")
|
|
270
|
+
.bind(bindingId, projectId).first<{ id: number; path: string }>();
|
|
271
|
+
if (!current) return response({ error: "Not found" }, 404);
|
|
272
|
+
gitlabProjectId = current.id;
|
|
273
|
+
gitlabProjectPath = current.path;
|
|
274
|
+
}
|
|
275
|
+
const id = bindingId ?? newId("glbind");
|
|
276
|
+
let webhookSecretPlaintext: string | null = null;
|
|
277
|
+
if (bindingId) {
|
|
278
|
+
const setToken = sealedToken ? ", access_token_ciphertext = ?, access_token_iv = ?" : "";
|
|
279
|
+
const updated = await env.DB.prepare(`
|
|
280
|
+
UPDATE project_gitlab_bindings SET gitlab_host = ?, gitlab_project_id = ?, gitlab_project_path = ?, trigger = ?, plan_ids_json = ?, base_url_template = ?,
|
|
281
|
+
execution_mode = ?, environment_id = ?, enabled = ?${setToken}
|
|
282
|
+
WHERE id = ? AND project_id = ?
|
|
283
|
+
`).bind(
|
|
284
|
+
host, gitlabProjectId, gitlabProjectPath, trigger, planIds ? JSON.stringify(planIds) : null, template, executionMode, environmentId, enabled ? 1 : 0,
|
|
285
|
+
...(sealedToken ? [sealedToken.ciphertext, sealedToken.iv] : []),
|
|
286
|
+
bindingId, projectId,
|
|
287
|
+
).run();
|
|
288
|
+
if ((updated.meta?.changes ?? 0) === 0) return response({ error: "Not found" }, 404);
|
|
289
|
+
} else {
|
|
290
|
+
webhookSecretPlaintext = randomToken(24);
|
|
291
|
+
const sealedSecret = await sealSecret(env, webhookSecretPlaintext);
|
|
292
|
+
try {
|
|
293
|
+
await env.DB.prepare(`
|
|
294
|
+
INSERT INTO project_gitlab_bindings (id, project_id, gitlab_host, gitlab_project_id, gitlab_project_path, access_token_ciphertext, access_token_iv,
|
|
295
|
+
webhook_secret_ciphertext, webhook_secret_iv, plan_ids_json, environment_id, trigger, base_url_template, execution_mode, enabled, created_by)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
297
|
+
`).bind(
|
|
298
|
+
id, projectId, host, gitlabProjectId, gitlabProjectPath, sealedToken!.ciphertext, sealedToken!.iv, sealedSecret.ciphertext, sealedSecret.iv,
|
|
299
|
+
planIds ? JSON.stringify(planIds) : null, environmentId, trigger, template, executionMode, enabled ? 1 : 0, principal.aegisId,
|
|
300
|
+
).run();
|
|
301
|
+
} catch {
|
|
302
|
+
return response({ error: "This GitLab project already runs checks for that trigger" }, 409);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await auditProject(env, principal, projectId, {
|
|
306
|
+
action: bindingId ? "gitlab.checks_updated" : "gitlab.checks_added", targetType: "gitlab_binding", targetId: id, data: { projectPath: gitlabProjectPath, trigger },
|
|
307
|
+
}, request);
|
|
308
|
+
return response({
|
|
309
|
+
binding: { id, host, gitlabProjectId, gitlabProjectPath, trigger, planIds, baseUrlTemplate: template, executionMode, environmentId, enabled },
|
|
310
|
+
webhookUrl: webhookUrl(env, id),
|
|
311
|
+
...(webhookSecretPlaintext ? { webhookSecret: webhookSecretPlaintext } : {}),
|
|
312
|
+
}, bindingId ? 200 : 201);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function rotateWebhookSecret({ env, principal, request }: AuthedRouteContext, projectId: string, bindingId: string): Promise<Response> {
|
|
316
|
+
if (!(await memberOfProject(env, principal, projectId, "project.manage"))) return response({ error: "Project scope is not allowed" }, 403);
|
|
317
|
+
const binding = await env.DB.prepare("SELECT id FROM project_gitlab_bindings WHERE id = ? AND project_id = ?").bind(bindingId, projectId).first();
|
|
318
|
+
if (!binding) return response({ error: "Not found" }, 404);
|
|
319
|
+
const secret = randomToken(24);
|
|
320
|
+
const sealed = await sealSecret(env, secret);
|
|
321
|
+
await env.DB.prepare("UPDATE project_gitlab_bindings SET webhook_secret_ciphertext = ?, webhook_secret_iv = ? WHERE id = ?").bind(sealed.ciphertext, sealed.iv, bindingId).run();
|
|
322
|
+
await auditProject(env, principal, projectId, { action: "gitlab.webhook_secret_rotated", targetType: "gitlab_binding", targetId: bindingId }, request);
|
|
323
|
+
return response({ webhookUrl: webhookUrl(env, bindingId), webhookSecret: secret });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
registerRoutes(async (context) => {
|
|
327
|
+
const { url, request } = context;
|
|
328
|
+
let params = matchPath(url.pathname, "/api/projects/:projectId/gitlab-bindings");
|
|
329
|
+
if (params && request.method === "GET") return listBindings(context, params.projectId);
|
|
330
|
+
if (params && request.method === "POST") return saveBinding(context, params.projectId);
|
|
331
|
+
params = matchPath(url.pathname, "/api/projects/:projectId/gitlab-bindings/:bindingId");
|
|
332
|
+
if (params && (request.method === "PATCH" || request.method === "DELETE")) return saveBinding(context, params.projectId, params.bindingId);
|
|
333
|
+
params = matchPath(url.pathname, "/api/projects/:projectId/gitlab-bindings/:bindingId/rotate-webhook-secret");
|
|
334
|
+
if (params && request.method === "POST") return rotateWebhookSecret(context, params.projectId, params.bindingId);
|
|
335
|
+
return null;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
registerPublicRoutes(async ({ request, env, url }) => {
|
|
339
|
+
if (url.pathname !== "/api/gitlab/webhook" || request.method !== "POST") return null;
|
|
340
|
+
const bindingId = url.searchParams.get("binding") ?? "";
|
|
341
|
+
const token = request.headers.get("x-gitlab-token") ?? "";
|
|
342
|
+
const binding = await env.DB.prepare("SELECT * FROM project_gitlab_bindings WHERE id = ? AND enabled = 1").bind(bindingId).first<Binding>();
|
|
343
|
+
if (!binding) return response({ error: "Unknown binding" }, 404);
|
|
344
|
+
const expected = await openSecret(env, binding.webhook_secret_ciphertext, binding.webhook_secret_iv);
|
|
345
|
+
if (!token || !constantTimeEqual(token, expected)) return response({ error: "Invalid webhook token" }, 401);
|
|
346
|
+
const event = parseMergeRequestEvent(await requestJson(request));
|
|
347
|
+
if (!event || event.gitlabProjectId !== binding.gitlab_project_id) return response({ started: [] });
|
|
348
|
+
return response(await handleMergeRequestEvent(env, binding, event));
|
|
349
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Extension points on the run and task lifecycle, so feature modules (cloud
|
|
2
|
+
// dispatch, metering, notifications) react without growing index.ts.
|
|
3
|
+
import type { Env } from "./core.js";
|
|
4
|
+
|
|
5
|
+
/** Work Bugmole Cloud can execute: a test run, or a discovery task. */
|
|
6
|
+
export type CloudJobKind = "run" | "task";
|
|
7
|
+
|
|
8
|
+
export type RunQueued = { runId: string; projectId: string; workspaceId: string | null; executionMode: "self" | "cloud" };
|
|
9
|
+
export type RunChanged = {
|
|
10
|
+
runId: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
workspaceId: string | null;
|
|
13
|
+
executionMode: string;
|
|
14
|
+
status: string;
|
|
15
|
+
previousStatus: string;
|
|
16
|
+
terminal: boolean;
|
|
17
|
+
/** Set when the cloud dispatcher itself changed the run, so its own hooks don't dispatch again. */
|
|
18
|
+
fromDispatcher?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TaskQueued = { taskId: string; projectId: string; workspaceId: string | null; executionMode: "self" | "cloud" };
|
|
22
|
+
export type TaskChanged = Omit<RunChanged, "runId"> & { taskId: string };
|
|
23
|
+
|
|
24
|
+
/** A progress update or heartbeat of unfinished cloud-capable work. */
|
|
25
|
+
export type JobHeartbeat = { kind: CloudJobKind; id: string; projectId: string; workspaceId: string | null; executionMode: string };
|
|
26
|
+
/** Tells a still-running worker to stop (usage cap, time limit). */
|
|
27
|
+
export type StopSignal = { reason: string; message: string };
|
|
28
|
+
|
|
29
|
+
export const runQueuedHooks: Array<(env: Env, event: RunQueued) => Promise<void>> = [];
|
|
30
|
+
export const runChangedHooks: Array<(env: Env, event: RunChanged) => Promise<void>> = [];
|
|
31
|
+
export const taskQueuedHooks: Array<(env: Env, event: TaskQueued) => Promise<void>> = [];
|
|
32
|
+
export const taskChangedHooks: Array<(env: Env, event: TaskChanged) => Promise<void>> = [];
|
|
33
|
+
/** Checked on every progress update and heartbeat of unfinished work. */
|
|
34
|
+
export const heartbeatChecks: Array<(env: Env, job: JobHeartbeat) => Promise<StopSignal | null>> = [];
|
|
35
|
+
/** Fired after a workspace's plan changes (checkout, downgrade, cancellation, lapse). */
|
|
36
|
+
export type PlanChanged = { workspaceId: string; from: string; to: string };
|
|
37
|
+
export const planChangedHooks: Array<(env: Env, event: PlanChanged) => Promise<void>> = [];
|
|
38
|
+
|
|
39
|
+
/** Run details only Bugmole's own integrations may set (a pull request's preview URL and commit). */
|
|
40
|
+
export type InternalRunFields = {
|
|
41
|
+
environmentBaseUrl?: string;
|
|
42
|
+
triggerSource?: string;
|
|
43
|
+
repoFullName?: string;
|
|
44
|
+
commitSha?: string;
|
|
45
|
+
prNumber?: number;
|
|
46
|
+
githubInstallationId?: number;
|
|
47
|
+
gitlabBindingId?: string;
|
|
48
|
+
};
|
|
49
|
+
export type RunCreator = (request: Request, env: Env, principal: import("./core.js").Principal, projectId: string, ctx: ExecutionContext, internal?: InternalRunFields) => Promise<Response>;
|
|
50
|
+
/** Set by index.ts, so integrations can create runs through the same checks as the API. */
|
|
51
|
+
export const runService: { create?: RunCreator } = {};
|
|
52
|
+
|
|
53
|
+
/** The registry's own request handler, set by index.ts, for callers that act as a user in-process (hosted MCP). */
|
|
54
|
+
export const registryDispatch: { fetch?: (request: Request, env: import("./core.js").Env, ctx: ExecutionContext) => Promise<Response> } = {};
|