@treeseed/sdk 0.10.28 → 0.12.4
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 +213 -7
- package/dist/agent-capacity.d.ts +916 -0
- package/dist/agent-capacity.js +1600 -0
- package/dist/agent-tools.d.ts +37 -0
- package/dist/agent-tools.js +301 -0
- package/dist/api/app.d.ts +1 -1
- package/dist/api/auth/d1-database.d.ts +2 -2
- package/dist/api/auth/d1-provider.d.ts +4 -4
- package/dist/api/auth/d1-store.d.ts +2 -2
- package/dist/api/auth/memory-provider.d.ts +1 -1
- package/dist/api/auth/tokens.d.ts +1 -1
- package/dist/api/config.d.ts +3 -1
- package/dist/api/config.js +34 -10
- package/dist/api/http.d.ts +2 -2
- package/dist/api/index.d.ts +10 -10
- package/dist/api/index.js +4 -2
- package/dist/api/operations-routes.d.ts +3 -3
- package/dist/api/operations.d.ts +1 -1
- package/dist/api/providers.d.ts +1 -1
- package/dist/api/providers.js +21 -4
- package/dist/api/railway.d.ts +3 -2
- package/dist/api/sdk-dispatch.d.ts +4 -4
- package/dist/api/sdk-routes.d.ts +2 -2
- package/dist/api/templates.d.ts +2 -2
- package/dist/api/types.d.ts +3 -2
- package/dist/capacity-provider.d.ts +118 -59
- package/dist/capacity-provider.js +235 -43
- package/dist/capacity.d.ts +2 -2
- package/dist/cli-tools.d.ts +1 -1
- package/dist/content-operations.d.ts +97 -0
- package/dist/content-operations.js +250 -0
- package/dist/content-store.d.ts +4 -4
- package/dist/control-plane-client.d.ts +13 -18
- package/dist/control-plane-client.js +8 -86
- package/dist/control-plane.d.ts +3 -2
- package/dist/control-plane.js +38 -13
- package/dist/copilot.d.ts +3 -0
- package/dist/copilot.js +16 -1
- package/dist/d1-http.d.ts +1 -1
- package/dist/d1-store.d.ts +32 -57
- package/dist/d1-store.js +115 -318
- package/dist/db/d1.d.ts +1 -232
- package/dist/db/index.d.ts +3 -3
- package/dist/db/market-schema.d.ts +50900 -11018
- package/dist/db/market-schema.js +2313 -467
- package/dist/db/node-sqlite.d.ts +1 -232
- package/dist/db/node-sqlite.js +7 -2
- package/dist/db/schema.d.ts +0 -462
- package/dist/db/schema.js +2 -22
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +0 -11
- package/dist/fixture-support.js +52 -4
- package/dist/governance.d.ts +133 -0
- package/dist/governance.js +285 -0
- package/dist/graph/build.d.ts +2 -2
- package/dist/graph/context-query-contracts.d.ts +1 -1
- package/dist/graph/dsl.d.ts +1 -1
- package/dist/graph/query.d.ts +4 -4
- package/dist/graph/ranking.d.ts +1 -1
- package/dist/graph/schema.d.ts +1 -1
- package/dist/graph.d.ts +11 -11
- package/dist/guarantees/index.d.ts +425 -0
- package/dist/guarantees/index.js +1146 -0
- package/dist/hosting/apps.d.ts +12 -0
- package/dist/hosting/apps.js +107 -0
- package/dist/hosting/builtins.d.ts +25 -0
- package/dist/hosting/builtins.js +602 -0
- package/dist/hosting/contracts.d.ts +208 -0
- package/dist/hosting/contracts.js +0 -0
- package/dist/hosting/graph.d.ts +195 -0
- package/dist/hosting/graph.js +1092 -0
- package/dist/hosting/index.d.ts +4 -0
- package/dist/hosting/index.js +4 -0
- package/dist/index.d.ts +88 -67
- package/dist/index.js +142 -14
- package/dist/local-dev/managed-dev.d.ts +85 -0
- package/dist/local-dev/managed-dev.js +368 -0
- package/dist/managed-dependencies.d.ts +9 -0
- package/dist/managed-dependencies.js +10 -9
- package/dist/market-client.d.ts +327 -9
- package/dist/market-client.js +464 -20
- package/dist/model-registry.d.ts +1 -1
- package/dist/model-registry.js +19 -19
- package/dist/operations/agent-tools.d.ts +2 -11
- package/dist/operations/agent-tools.js +2 -14
- package/dist/operations/providers/default.d.ts +1 -1
- package/dist/operations/providers/default.js +20 -37
- package/dist/operations/repository-operations.d.ts +26 -2
- package/dist/operations/repository-operations.js +159 -1
- package/dist/operations/runtime.d.ts +3 -3
- package/dist/operations/services/bootstrap-runner.d.ts +4 -2
- package/dist/operations/services/bootstrap-runner.js +22 -2
- package/dist/operations/services/build-warning-policy.d.ts +34 -0
- package/dist/operations/services/build-warning-policy.js +81 -82
- package/dist/operations/services/commit-message-provider.js +4 -4
- package/dist/operations/services/config-runtime.d.ts +83 -24
- package/dist/operations/services/config-runtime.js +449 -130
- package/dist/operations/services/deploy.d.ts +106 -624
- package/dist/operations/services/deploy.js +746 -263
- package/dist/operations/services/deployment-readiness.d.ts +30 -0
- package/dist/operations/services/deployment-readiness.js +206 -0
- package/dist/operations/services/git-remote-policy.js +12 -4
- package/dist/operations/services/git-runner.d.ts +66 -0
- package/dist/operations/services/git-runner.js +479 -0
- package/dist/operations/services/git-workflow.d.ts +45 -6
- package/dist/operations/services/git-workflow.js +251 -23
- package/dist/operations/services/github-actions-verification.d.ts +4 -1
- package/dist/operations/services/github-actions-verification.js +12 -3
- package/dist/operations/services/github-api.js +22 -6
- package/dist/operations/services/github-automation.d.ts +4 -4
- package/dist/operations/services/github-automation.js +19 -21
- package/dist/operations/services/github-credentials.d.ts +13 -0
- package/dist/operations/services/github-credentials.js +59 -0
- package/dist/operations/services/hosted-service-checks.d.ts +81 -0
- package/dist/operations/services/hosted-service-checks.js +478 -0
- package/dist/operations/services/hosting-audit.d.ts +2 -2
- package/dist/operations/services/hosting-audit.js +7 -7
- package/dist/operations/services/hub-launch.d.ts +1 -1
- package/dist/operations/services/hub-provider-launch.d.ts +5 -5
- package/dist/operations/services/hub-provider-launch.js +22 -17
- package/dist/operations/services/live-hosted-service-checks.d.ts +26 -0
- package/dist/operations/services/live-hosted-service-checks.js +463 -0
- package/dist/operations/services/local-dev.js +0 -1
- package/dist/operations/services/managed-host-security.d.ts +1 -1
- package/dist/operations/services/managed-host-security.js +1 -1
- package/dist/operations/services/managed-repositories.d.ts +37 -0
- package/dist/operations/services/managed-repositories.js +177 -0
- package/dist/operations/services/operations-runner-smoke.d.ts +31 -0
- package/dist/operations/services/operations-runner-smoke.js +198 -0
- package/dist/operations/services/package-adapters.d.ts +170 -0
- package/dist/operations/services/package-adapters.js +859 -0
- package/dist/operations/services/package-reference-policy.d.ts +13 -88
- package/dist/operations/services/package-reference-policy.js +96 -215
- package/dist/operations/services/project-host-operations.d.ts +4 -4
- package/dist/operations/services/project-platform.d.ts +57 -173
- package/dist/operations/services/project-platform.js +80 -111
- package/dist/operations/services/project-web-monitor.d.ts +2 -2
- package/dist/operations/services/project-web-monitor.js +122 -5
- package/dist/operations/services/railway-api.d.ts +117 -5
- package/dist/operations/services/railway-api.js +915 -93
- package/dist/operations/services/railway-deploy.d.ts +30 -112
- package/dist/operations/services/railway-deploy.js +521 -961
- package/dist/operations/services/release-history.js +3 -4
- package/dist/operations/services/release-proof-ledger.d.ts +13 -0
- package/dist/operations/services/release-proof-ledger.js +81 -0
- package/dist/operations/services/release-proof-planner.d.ts +48 -0
- package/dist/operations/services/release-proof-planner.js +195 -0
- package/dist/operations/services/release-proof-runner.d.ts +16 -0
- package/dist/operations/services/release-proof-runner.js +169 -0
- package/dist/operations/services/release-proof.d.ts +81 -0
- package/dist/operations/services/release-proof.js +79 -0
- package/dist/operations/services/repository-save-orchestrator.d.ts +8 -7
- package/dist/operations/services/repository-save-orchestrator.js +604 -212
- package/dist/operations/services/runtime-paths.d.ts +1 -3
- package/dist/operations/services/runtime-paths.js +0 -4
- package/dist/operations/services/runtime-tools.d.ts +1 -191
- package/dist/operations/services/runtime-tools.js +4 -443
- package/dist/operations/services/save-deploy-preflight.js +9 -1
- package/dist/operations/services/template-host-bindings.d.ts +2 -2
- package/dist/operations/services/template-host-bindings.js +3 -2
- package/dist/operations/services/template-registry.d.ts +6 -6
- package/dist/operations/services/template-registry.js +33 -14
- package/dist/operations/services/template-secret-sync.d.ts +3 -3
- package/dist/operations/services/verification-cache.d.ts +25 -0
- package/dist/operations/services/verification-cache.js +71 -0
- package/dist/operations/services/workflow-timing.d.ts +25 -0
- package/dist/operations/services/workflow-timing.js +79 -0
- package/dist/operations/services/workspace-dependency-mode.js +50 -5
- package/dist/operations/services/workspace-preflight.js +8 -8
- package/dist/operations/services/workspace-save-script.d.ts +4 -4
- package/dist/operations/services/workspace-save.d.ts +1 -0
- package/dist/operations/services/workspace-save.js +42 -19
- package/dist/operations/services/workspace-tools.js +42 -7
- package/dist/operations-registry.d.ts +1 -1
- package/dist/operations-registry.js +5 -6
- package/dist/operations-types.d.ts +1 -1
- package/dist/operations.d.ts +17 -11
- package/dist/operations.js +57 -3
- package/dist/platform/book-export.js +2 -0
- package/dist/platform/books-data.d.ts +1 -1
- package/dist/platform/config.d.ts +27 -0
- package/dist/platform/config.js +100 -0
- package/dist/platform/content-runtime-source.d.ts +54 -0
- package/dist/platform/content-runtime-source.js +169 -0
- package/dist/platform/contracts.d.ts +38 -2
- package/dist/platform/deploy-config.d.ts +1 -1
- package/dist/platform/deploy-config.js +80 -9
- package/dist/platform/deploy-runtime.d.ts +1 -1
- package/dist/platform/desired-state.d.ts +63 -0
- package/dist/platform/desired-state.js +1021 -0
- package/dist/platform/env.yaml +568 -45
- package/dist/platform/environment.d.ts +3 -2
- package/dist/platform/environment.js +122 -21
- package/dist/platform/local-content-materialization.d.ts +9 -0
- package/dist/platform/local-content-materialization.js +212 -0
- package/dist/platform/plugin.d.ts +13 -4
- package/dist/platform/plugins/runtime.d.ts +3 -3
- package/dist/platform/plugins/runtime.js +62 -1
- package/dist/platform/plugins.d.ts +6 -6
- package/dist/platform/published-content-pipeline.d.ts +3 -3
- package/dist/platform/published-content-pipeline.js +5 -1
- package/dist/platform/published-content.d.ts +4 -3
- package/dist/platform/site-config-schema.d.ts +1 -0
- package/dist/platform/site-config-schema.js +4 -1
- package/dist/platform/tenant/runtime-config.d.ts +152 -2
- package/dist/platform/tenant-config.d.ts +1 -1
- package/dist/platform/utils/site-config-schema.d.ts +154 -0
- package/dist/platform/utils/site-config-schema.js +400 -461
- package/dist/platform-operation-store.d.ts +1 -1
- package/dist/platform-operation-store.js +2 -2
- package/dist/platform-operations.js +1 -1
- package/dist/plugin-default.d.ts +1 -1
- package/dist/plugin-default.js +27 -12
- package/dist/project-import.d.ts +91 -0
- package/dist/project-import.js +257 -0
- package/dist/project-workflow.d.ts +1 -1
- package/dist/reconcile/bootstrap-systems.d.ts +2 -2
- package/dist/reconcile/bootstrap-systems.js +5 -5
- package/dist/reconcile/builtin-adapters.d.ts +7 -1
- package/dist/reconcile/builtin-adapters.js +3190 -429
- package/dist/reconcile/contracts.d.ts +23 -6
- package/dist/reconcile/desired-state.d.ts +2 -192
- package/dist/reconcile/desired-state.js +29 -38
- package/dist/reconcile/engine.d.ts +61 -395
- package/dist/reconcile/engine.js +229 -44
- package/dist/reconcile/index.d.ts +10 -8
- package/dist/reconcile/index.js +2 -0
- package/dist/reconcile/live-acceptance.d.ts +100 -0
- package/dist/reconcile/live-acceptance.js +2027 -0
- package/dist/reconcile/platform.d.ts +104 -0
- package/dist/reconcile/platform.js +100 -0
- package/dist/reconcile/providers/docker-private.d.ts +54 -0
- package/dist/reconcile/providers/docker-private.js +101 -0
- package/dist/reconcile/providers/github-private.d.ts +65 -0
- package/dist/reconcile/providers/github-private.js +119 -0
- package/dist/reconcile/providers/local-private.d.ts +28 -0
- package/dist/reconcile/providers/local-private.js +75 -0
- package/dist/reconcile/providers/railway-iac.d.ts +66 -0
- package/dist/reconcile/providers/railway-iac.js +275 -0
- package/dist/reconcile/providers/release-private.d.ts +92 -0
- package/dist/reconcile/providers/release-private.js +152 -0
- package/dist/reconcile/registry.d.ts +2 -2
- package/dist/reconcile/registry.js +16 -1
- package/dist/reconcile/state.d.ts +1 -1
- package/dist/reconcile/state.js +7 -44
- package/dist/reconcile/units.d.ts +1 -1
- package/dist/reconcile/units.js +9 -3
- package/dist/remote.d.ts +6 -6
- package/dist/scenes/artifacts.d.ts +16 -0
- package/dist/scenes/artifacts.js +61 -0
- package/dist/scenes/auth.d.ts +2 -0
- package/dist/scenes/auth.js +28 -0
- package/dist/scenes/base-url.d.ts +12 -0
- package/dist/scenes/base-url.js +123 -0
- package/dist/scenes/builtin-plugins.d.ts +2 -0
- package/dist/scenes/builtin-plugins.js +447 -0
- package/dist/scenes/checkpoints.d.ts +14 -0
- package/dist/scenes/checkpoints.js +33 -0
- package/dist/scenes/device-matrix.d.ts +2 -0
- package/dist/scenes/device-matrix.js +97 -0
- package/dist/scenes/devices.d.ts +11 -0
- package/dist/scenes/devices.js +22 -0
- package/dist/scenes/diagnostics.d.ts +5 -0
- package/dist/scenes/diagnostics.js +22 -0
- package/dist/scenes/diagram-providers.d.ts +2 -0
- package/dist/scenes/diagram-providers.js +181 -0
- package/dist/scenes/diagram-validation.d.ts +22 -0
- package/dist/scenes/diagram-validation.js +74 -0
- package/dist/scenes/environment.d.ts +2 -0
- package/dist/scenes/environment.js +65 -0
- package/dist/scenes/evidence-bundle.d.ts +5 -0
- package/dist/scenes/evidence-bundle.js +42 -0
- package/dist/scenes/evidence-report.d.ts +10 -0
- package/dist/scenes/evidence-report.js +73 -0
- package/dist/scenes/evidence.d.ts +16 -0
- package/dist/scenes/evidence.js +277 -0
- package/dist/scenes/index.d.ts +48 -0
- package/dist/scenes/index.js +312 -0
- package/dist/scenes/inspect.d.ts +9 -0
- package/dist/scenes/inspect.js +64 -0
- package/dist/scenes/loader.d.ts +3 -0
- package/dist/scenes/loader.js +38 -0
- package/dist/scenes/logs.d.ts +2 -0
- package/dist/scenes/logs.js +47 -0
- package/dist/scenes/operations.d.ts +3 -0
- package/dist/scenes/operations.js +128 -0
- package/dist/scenes/phase0.d.ts +9 -0
- package/dist/scenes/phase0.js +637 -0
- package/dist/scenes/planner.d.ts +12 -0
- package/dist/scenes/planner.js +166 -0
- package/dist/scenes/playwright-adapter.d.ts +2 -0
- package/dist/scenes/playwright-adapter.js +63 -0
- package/dist/scenes/plugins.d.ts +5 -0
- package/dist/scenes/plugins.js +115 -0
- package/dist/scenes/progress.d.ts +17 -0
- package/dist/scenes/progress.js +24 -0
- package/dist/scenes/publish-destinations.d.ts +12 -0
- package/dist/scenes/publish-destinations.js +99 -0
- package/dist/scenes/publish-plan-report.d.ts +10 -0
- package/dist/scenes/publish-plan-report.js +70 -0
- package/dist/scenes/publish-plan.d.ts +15 -0
- package/dist/scenes/publish-plan.js +248 -0
- package/dist/scenes/publish-redaction.d.ts +16 -0
- package/dist/scenes/publish-redaction.js +148 -0
- package/dist/scenes/publish-release-record.d.ts +13 -0
- package/dist/scenes/publish-release-record.js +34 -0
- package/dist/scenes/publish-report.d.ts +10 -0
- package/dist/scenes/publish-report.js +67 -0
- package/dist/scenes/publish.d.ts +16 -0
- package/dist/scenes/publish.js +298 -0
- package/dist/scenes/registry.d.ts +12 -0
- package/dist/scenes/registry.js +65 -0
- package/dist/scenes/remotion-adapter.d.ts +3 -0
- package/dist/scenes/remotion-adapter.js +94 -0
- package/dist/scenes/remotion-composition-registry.d.ts +2 -0
- package/dist/scenes/remotion-composition-registry.js +12 -0
- package/dist/scenes/remotion-compositions.d.ts +15 -0
- package/dist/scenes/remotion-compositions.js +464 -0
- package/dist/scenes/remotion-diagrams.d.ts +9 -0
- package/dist/scenes/remotion-diagrams.js +188 -0
- package/dist/scenes/remotion-input.d.ts +13 -0
- package/dist/scenes/remotion-input.js +291 -0
- package/dist/scenes/remotion-visuals.d.ts +29 -0
- package/dist/scenes/remotion-visuals.js +198 -0
- package/dist/scenes/render-media-assets.d.ts +9 -0
- package/dist/scenes/render-media-assets.js +117 -0
- package/dist/scenes/render-report.d.ts +10 -0
- package/dist/scenes/render-report.js +27 -0
- package/dist/scenes/render.d.ts +2 -0
- package/dist/scenes/render.js +218 -0
- package/dist/scenes/reporter.d.ts +3 -0
- package/dist/scenes/reporter.js +101 -0
- package/dist/scenes/resume.d.ts +2 -0
- package/dist/scenes/resume.js +79 -0
- package/dist/scenes/runner.d.ts +2 -0
- package/dist/scenes/runner.js +674 -0
- package/dist/scenes/schema.d.ts +6 -0
- package/dist/scenes/schema.js +994 -0
- package/dist/scenes/seed.d.ts +2 -0
- package/dist/scenes/seed.js +83 -0
- package/dist/scenes/segments.d.ts +15 -0
- package/dist/scenes/segments.js +79 -0
- package/dist/scenes/selectors.d.ts +3 -0
- package/dist/scenes/selectors.js +21 -0
- package/dist/scenes/timeline.d.ts +11 -0
- package/dist/scenes/timeline.js +26 -0
- package/dist/scenes/timeouts.d.ts +6 -0
- package/dist/scenes/timeouts.js +17 -0
- package/dist/scenes/training-captions.d.ts +14 -0
- package/dist/scenes/training-captions.js +98 -0
- package/dist/scenes/training-report.d.ts +10 -0
- package/dist/scenes/training-report.js +78 -0
- package/dist/scenes/training-transcript.d.ts +24 -0
- package/dist/scenes/training-transcript.js +204 -0
- package/dist/scenes/training.d.ts +10 -0
- package/dist/scenes/training.js +134 -0
- package/dist/scenes/types.d.ts +1872 -0
- package/dist/scenes/types.js +8 -0
- package/dist/scenes/visual-audit-contact-sheets.d.ts +6 -0
- package/dist/scenes/visual-audit-contact-sheets.js +119 -0
- package/dist/scenes/visual-audit-fixtures.d.ts +25 -0
- package/dist/scenes/visual-audit-fixtures.js +210 -0
- package/dist/scenes/visual-audit-report.d.ts +11 -0
- package/dist/scenes/visual-audit-report.js +85 -0
- package/dist/scenes/visual-audit-review.d.ts +23 -0
- package/dist/scenes/visual-audit-review.js +829 -0
- package/dist/scenes/visual-audit-routes.d.ts +13 -0
- package/dist/scenes/visual-audit-routes.js +308 -0
- package/dist/scenes/visual-audit.d.ts +3 -0
- package/dist/scenes/visual-audit.js +596 -0
- package/dist/scripts/build-dist.js +154 -40
- package/dist/scripts/check-build-warnings.js +42 -53
- package/dist/scripts/cleanup-markdown.js +1 -1
- package/dist/scripts/cli-test-fixtures.js +24 -28
- package/dist/scripts/deployment-readiness.js +20 -0
- package/dist/scripts/generate-treedx-openapi-types.js +186 -0
- package/dist/scripts/migrate-journey-csv-to-guarantees.js +140 -0
- package/dist/scripts/operations-runner-smoke.js +16 -0
- package/dist/scripts/package-tools.js +12 -1
- package/dist/scripts/prepare.js +3 -7
- package/dist/scripts/release-verify.js +6 -6
- package/dist/scripts/run-fixture-astro-command.js +1 -1
- package/dist/scripts/template-catalog.test.js +68 -82
- package/dist/scripts/tenant-dev.js +0 -10
- package/dist/scripts/tenant-workflow-action.js +10 -1
- package/dist/scripts/test-cloudflare-local.js +1 -1
- package/dist/scripts/test-scaffold.js +0 -1
- package/dist/scripts/test-smoke.js +3 -3
- package/dist/scripts/treeseed-build-dist.js +4 -7
- package/dist/scripts/treeseed-release-verify.js +1 -1
- package/dist/scripts/verify-driver.js +30 -21
- package/dist/scripts/workflow-commands.test.js +18 -27
- package/dist/scripts/workspace-command-e2e.js +1 -1
- package/dist/scripts/workspace-lint.js +6 -0
- package/dist/scripts/workspace-release-test.js +90 -1
- package/dist/scripts/workspace-test.js +6 -0
- package/dist/sdk-dispatch.d.ts +1 -1
- package/dist/sdk-dispatch.js +0 -16
- package/dist/sdk-fields.d.ts +1 -1
- package/dist/sdk-filters.d.ts +1 -1
- package/dist/sdk-types.d.ts +1352 -116
- package/dist/sdk-types.js +413 -2
- package/dist/sdk.d.ts +106 -100
- package/dist/sdk.js +211 -51
- package/dist/secrets-capability.d.ts +421 -0
- package/dist/secrets-capability.js +530 -0
- package/dist/seeds/normalize.js +1 -0
- package/dist/seeds/planner.js +38 -0
- package/dist/seeds/schema.js +293 -4
- package/dist/seeds/types.d.ts +60 -0
- package/dist/seeds/types.js +9 -1
- package/dist/service-credentials.d.ts +54 -0
- package/dist/service-credentials.js +112 -0
- package/dist/stores/cursor-store.d.ts +2 -2
- package/dist/stores/envelopes.d.ts +1 -1
- package/dist/stores/helpers.d.ts +1 -1
- package/dist/stores/lease-store.d.ts +2 -2
- package/dist/stores/message-store.d.ts +2 -2
- package/dist/stores/operational-store.d.ts +3 -26
- package/dist/stores/operational-store.js +0 -225
- package/dist/stores/project-workflow-store.d.ts +2 -2
- package/dist/stores/run-store.d.ts +2 -3
- package/dist/stores/run-store.js +0 -98
- package/dist/stores/subscription-store.d.ts +2 -3
- package/dist/stores/subscription-store.js +0 -100
- package/dist/template-catalog.d.ts +1 -1
- package/dist/template-launch-requirements.d.ts +2 -2
- package/dist/template-launch-requirements.js +9 -0
- package/dist/template-launch-ui.d.ts +2 -2
- package/dist/treedx/adapters.d.ts +6 -0
- package/dist/treedx/adapters.js +36 -0
- package/dist/treedx/client.d.ts +222 -0
- package/dist/treedx/client.js +871 -0
- package/dist/treedx/errors.d.ts +13 -0
- package/dist/treedx/errors.js +17 -0
- package/dist/treedx/federated-client.d.ts +27 -0
- package/dist/treedx/federated-client.js +158 -0
- package/dist/treedx/generated/openapi-types.d.ts +3558 -0
- package/dist/treedx/generated/openapi-types.js +0 -0
- package/dist/treedx/graph-adapter.d.ts +33 -0
- package/dist/treedx/graph-adapter.js +156 -0
- package/dist/treedx/index.d.ts +14 -0
- package/dist/treedx/index.js +48 -0
- package/dist/treedx/market-integration.d.ts +27 -0
- package/dist/treedx/market-integration.js +131 -0
- package/dist/treedx/ports.d.ts +166 -0
- package/dist/treedx/ports.js +231 -0
- package/dist/treedx/query-adapter.d.ts +19 -0
- package/dist/treedx/query-adapter.js +62 -0
- package/dist/treedx/registry-client.d.ts +11 -0
- package/dist/treedx/registry-client.js +19 -0
- package/dist/treedx/repository-adapter.d.ts +45 -0
- package/dist/treedx/repository-adapter.js +319 -0
- package/dist/treedx/sdk-integration.d.ts +27 -0
- package/dist/treedx/sdk-integration.js +63 -0
- package/dist/treedx/types.d.ts +1084 -0
- package/dist/treedx/types.js +8 -0
- package/dist/treedx/workspace-adapter.d.ts +27 -0
- package/dist/treedx/workspace-adapter.js +65 -0
- package/dist/treedx-backends.d.ts +220 -0
- package/dist/treedx-backends.js +634 -0
- package/dist/treedx-client.d.ts +89 -0
- package/dist/treedx-client.js +184 -0
- package/dist/treeseed/services/compose.yml +2 -3
- package/dist/treeseed/template-catalog/catalog.fixture.json +27 -27
- package/dist/types/agents.d.ts +256 -0
- package/dist/types/agents.js +42 -1
- package/dist/verification.d.ts +1 -0
- package/dist/verification.js +143 -14
- package/dist/workflow/operations.d.ts +580 -523
- package/dist/workflow/operations.js +2084 -1279
- package/dist/workflow/policy.js +19 -7
- package/dist/workflow/runs.d.ts +9 -3
- package/dist/workflow/runs.js +66 -26
- package/dist/workflow/session.d.ts +4 -1
- package/dist/workflow/session.js +50 -10
- package/dist/workflow/worktrees.d.ts +6 -2
- package/dist/workflow/worktrees.js +98 -15
- package/dist/workflow-state.d.ts +17 -4
- package/dist/workflow-state.js +68 -41
- package/dist/workflow-support.d.ts +26 -17
- package/dist/workflow-support.js +84 -24
- package/dist/workflow.d.ts +51 -16
- package/dist/workflow.js +19 -7
- package/drizzle/d1/0000_treeseed_d1.sql +0 -18
- package/drizzle/market/0000_market_control_plane.sql +1793 -1499
- package/drizzle/market/0004_treedx_market_integration.sql +99 -0
- package/drizzle/market/0005_agent_capacity_coordination.sql +145 -0
- package/drizzle/market/0006_provider_assignment_lifecycle.sql +9 -0
- package/drizzle/market/0007_agent_architecture_gap_closure.sql +157 -0
- package/drizzle/market/0008_agent_capacity_operational_closure.sql +27 -0
- package/drizzle/market/0009_agent_capacity_runtime_hardening.sql +29 -0
- package/drizzle/market/0010_secrets_capability_persistence.sql +130 -0
- package/drizzle/market/0011_github_app_adapter.sql +47 -0
- package/drizzle/market/0012_workday_test_runs.sql +44 -0
- package/drizzle/market/0013_runtime_task_lifecycle.sql +39 -0
- package/drizzle/market/0014_governance_pipeline.sql +189 -0
- package/package.json +106 -18
- package/scripts/{verify-driver.mjs → verify-driver.ts} +19 -4
- package/templates/github/deploy-web.workflow.yml +49 -12
- package/dist/operations/services/mailpit-runtime.d.ts +0 -20
- package/dist/operations/services/mailpit-runtime.js +0 -67
- package/dist/operations/services/release-candidate.d.ts +0 -41
- package/dist/operations/services/release-candidate.js +0 -616
- package/dist/scripts/ensure-mailpit.js +0 -48
- package/dist/scripts/logs-mailpit.js +0 -2
- package/dist/scripts/run-ts.js +0 -45
- package/dist/scripts/stop-mailpit.js +0 -5
- package/dist/scripts/treeseed-run-ts.js +0 -45
|
@@ -0,0 +1,2027 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
createTreeseedCanonicalReconcileReport
|
|
9
|
+
} from "./platform.js";
|
|
10
|
+
import { runTreeseedGit } from "../operations/services/git-runner.js";
|
|
11
|
+
import {
|
|
12
|
+
deleteRailwayProject,
|
|
13
|
+
ensureRailwayEnvironment,
|
|
14
|
+
ensureRailwayGeneratedServiceDomain,
|
|
15
|
+
ensureRailwayPostgresService,
|
|
16
|
+
ensureRailwayProject,
|
|
17
|
+
ensureRailwayService,
|
|
18
|
+
ensureRailwayServiceVolume,
|
|
19
|
+
listRailwayEnvironments,
|
|
20
|
+
listRailwayProjects,
|
|
21
|
+
listRailwayServices,
|
|
22
|
+
listRailwayVariables,
|
|
23
|
+
listRailwayVolumes,
|
|
24
|
+
railwayGraphqlRequest,
|
|
25
|
+
resolveRailwayWorkspaceContext,
|
|
26
|
+
upsertRailwayVariables
|
|
27
|
+
} from "../operations/services/railway-api.js";
|
|
28
|
+
import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
|
|
29
|
+
import { MarketClient } from "../market-client.js";
|
|
30
|
+
import { MarketProviderClient } from "../capacity-provider.js";
|
|
31
|
+
const PROVIDER_CAPABILITIES = {
|
|
32
|
+
railway: ["project", "environment", "service", "image-service", "postgres", "volume", "domain", "variables", "deployment-health", "capacity-provider-runtime-assignment-proof"],
|
|
33
|
+
cloudflare: ["pages", "worker", "d1", "r2", "kv", "queue", "dns", "turnstile", "secrets", "cache-rules"],
|
|
34
|
+
github: ["environment", "secret", "variable", "workflow-dispatch", "workflow-observation", "repository-scoped-token"],
|
|
35
|
+
local: ["process", "port", "local-db", "local-runner", "docker-compose-capacity-provider", "capacity-provider-assignment-proof"]
|
|
36
|
+
};
|
|
37
|
+
function isCapacityRuntimeProofCapability(capability) {
|
|
38
|
+
return capability === "capacity-provider-assignment-proof" || capability === "capacity-provider-runtime-assignment-proof";
|
|
39
|
+
}
|
|
40
|
+
function scenarioResourceKind(provider, capability) {
|
|
41
|
+
if (provider === "railway") return capability === "volume" ? "railway-volume" : "railway-service";
|
|
42
|
+
if (provider === "cloudflare") return "cloudflare-resource";
|
|
43
|
+
if (provider === "github") {
|
|
44
|
+
if (capability === "environment") return "github-environment";
|
|
45
|
+
if (capability === "secret") return "github-secret-binding";
|
|
46
|
+
if (capability === "variable") return "github-secret-binding";
|
|
47
|
+
return "package-workflow";
|
|
48
|
+
}
|
|
49
|
+
if (isCapacityRuntimeProofCapability(capability)) return "capacity-provider";
|
|
50
|
+
if (capability === "docker-compose-capacity-provider") return "local-docker-compose";
|
|
51
|
+
if (capability === "process") return "local-process";
|
|
52
|
+
if (capability === "local-runner") return "capacity-provider";
|
|
53
|
+
return "local-process";
|
|
54
|
+
}
|
|
55
|
+
function scenarioResourceProvider(provider, capability) {
|
|
56
|
+
if (provider === "cloudflare") return "cloudflare";
|
|
57
|
+
if (provider === "github") return "github";
|
|
58
|
+
if (provider === "local") return "local";
|
|
59
|
+
return "railway";
|
|
60
|
+
}
|
|
61
|
+
function liveAcceptanceDesiredResource(input) {
|
|
62
|
+
const id = `live-acceptance:${input.environment}:${input.provider}:${input.capability}:${input.runId}`;
|
|
63
|
+
const kind = scenarioResourceKind(input.provider, input.capability);
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
kind,
|
|
67
|
+
provider: scenarioResourceProvider(input.provider, input.capability),
|
|
68
|
+
environment: input.environment,
|
|
69
|
+
packageId: null,
|
|
70
|
+
serviceId: input.capability,
|
|
71
|
+
logicalName: input.capability,
|
|
72
|
+
spec: {
|
|
73
|
+
liveAcceptance: true,
|
|
74
|
+
provider: input.provider,
|
|
75
|
+
capability: input.capability,
|
|
76
|
+
runId: input.runId,
|
|
77
|
+
prefix: providerPrefix(input.environment, input.provider, input.runId),
|
|
78
|
+
tenantRoot: input.tenantRoot
|
|
79
|
+
},
|
|
80
|
+
dependencies: [],
|
|
81
|
+
source: { type: "package-adapter", id }
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function compileTreeseedLiveAcceptanceScenarios(input) {
|
|
85
|
+
const providers = input.provider === "all" ? ["railway", "cloudflare", "github", "local"] : [input.provider];
|
|
86
|
+
return providers.flatMap((provider) => PROVIDER_CAPABILITIES[provider].map((capability) => {
|
|
87
|
+
const probeOnly = input.mode === "smoke" || provider === "github" && ["workflow-observation", "repository-scoped-token"].includes(capability) || provider === "cloudflare" && capability === "cache-rules" || isCapacityRuntimeProofCapability(capability);
|
|
88
|
+
const desiredResources = probeOnly ? [] : [liveAcceptanceDesiredResource({
|
|
89
|
+
tenantRoot: input.tenantRoot,
|
|
90
|
+
environment: input.environment,
|
|
91
|
+
provider,
|
|
92
|
+
capability,
|
|
93
|
+
runId: input.runId
|
|
94
|
+
})];
|
|
95
|
+
const selector = {
|
|
96
|
+
environment: input.environment,
|
|
97
|
+
host: [provider],
|
|
98
|
+
resourceKind: desiredResources.map((resource) => resource.kind),
|
|
99
|
+
serviceType: [capability]
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
id: `live-acceptance:${input.environment}:${provider}:${capability}:${input.runId}`,
|
|
103
|
+
provider,
|
|
104
|
+
capability,
|
|
105
|
+
desiredResources,
|
|
106
|
+
selector,
|
|
107
|
+
expectedActions: input.mode === "cleanup" ? ["delete", "noop"] : probeOnly ? ["noop"] : ["create", "update", "replace", "reattach", "noop"],
|
|
108
|
+
cleanupSelector: selector,
|
|
109
|
+
required: true,
|
|
110
|
+
probeOnly,
|
|
111
|
+
cleanupRequired: input.mode !== "smoke" && !probeOnly
|
|
112
|
+
};
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function configuredValue(env, keys) {
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
const value = env[key];
|
|
118
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
119
|
+
}
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
function capacityAcceptanceConfig(env, environment) {
|
|
123
|
+
const local = environment === "local";
|
|
124
|
+
const apiUrl = configuredValue(env, [
|
|
125
|
+
"TREESEED_CAPACITY_ACCEPTANCE_API_URL",
|
|
126
|
+
"TREESEED_MARKET_URL",
|
|
127
|
+
"TREESEED_MANAGEMENT_API_URL",
|
|
128
|
+
"TREESEED_API_BASE_URL"
|
|
129
|
+
]) || (local ? "http://127.0.0.1:3000" : "");
|
|
130
|
+
const adminToken = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN"]) || (local ? "tsk_local_treeseed_acceptance_admin" : "");
|
|
131
|
+
const teamId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID"]) || (local ? "d8a613c2-bbdb-4474-9c96-31e985beafd4" : "");
|
|
132
|
+
const projectId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID"]) || (local ? "90764af2-5a13-42b9-a2fa-fb3af5882323" : "");
|
|
133
|
+
const providerId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID", "TREESEED_CAPACITY_PROVIDER_ID"]) || (local ? "ff48ce97-6959-46b5-be9f-9b7062161fe3" : "");
|
|
134
|
+
const agentClassId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID"]) || (local ? "planning" : "");
|
|
135
|
+
const providerApiKey = configuredValue(env, ["TREESEED_CAPACITY_PROVIDER_API_KEY"]) || (local ? "tsp_local_treeseed_demo_capacity_provider" : "");
|
|
136
|
+
const missing = [
|
|
137
|
+
["TREESEED_CAPACITY_ACCEPTANCE_API_URL or TREESEED_MARKET_URL", apiUrl],
|
|
138
|
+
["TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN", adminToken],
|
|
139
|
+
["TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID", teamId],
|
|
140
|
+
["TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID", projectId],
|
|
141
|
+
["TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID or TREESEED_CAPACITY_PROVIDER_ID", providerId],
|
|
142
|
+
["TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID", agentClassId],
|
|
143
|
+
["TREESEED_CAPACITY_PROVIDER_API_KEY", providerApiKey]
|
|
144
|
+
].filter(([, value]) => !value).map(([key]) => String(key));
|
|
145
|
+
return {
|
|
146
|
+
apiUrl,
|
|
147
|
+
adminToken,
|
|
148
|
+
teamId,
|
|
149
|
+
projectId,
|
|
150
|
+
providerId,
|
|
151
|
+
agentClassId,
|
|
152
|
+
providerApiKey,
|
|
153
|
+
missing
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function capacityGrantForAcceptance(grants, config, environment) {
|
|
157
|
+
return grants.map((entry) => entry && typeof entry === "object" ? entry : null).find(
|
|
158
|
+
(grant) => grant && grant.id && grant.capacityProviderId === config.providerId && grant.projectId === config.projectId && (grant.teamId === config.teamId || !grant.teamId) && (grant.state === "active" || !grant.state) && (grant.environment === environment || !grant.environment)
|
|
159
|
+
) ?? null;
|
|
160
|
+
}
|
|
161
|
+
async function createTreeDxProxyAuditEvidence(input) {
|
|
162
|
+
const response = await input.fetchImpl(
|
|
163
|
+
`${input.apiUrl.replace(/\/$/u, "")}/v1/dx/projects/${encodeURIComponent(input.projectId)}/repos/${encodeURIComponent(`capacity-proof-${input.runId}`)}/files/read`,
|
|
164
|
+
{
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
accept: "application/json",
|
|
168
|
+
authorization: `Bearer ${input.providerApiKey}`,
|
|
169
|
+
"content-type": "application/json",
|
|
170
|
+
"x-treeseed-assignment-id": input.assignmentId
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
paths: [`src/content/agent-platform-proof/${input.runId}.mdx`],
|
|
174
|
+
ref: "HEAD"
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
if (response.status !== 403) {
|
|
179
|
+
const text = await response.text().catch(() => "");
|
|
180
|
+
throw new Error(`Capacity acceptance TreeDX proxy audit probe expected 403 but received ${response.status}${text ? `: ${text}` : ""}.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function runCapacityProviderAssignmentProof(input) {
|
|
184
|
+
const config = capacityAcceptanceConfig(input.env, input.environment);
|
|
185
|
+
if (config.missing.length > 0) {
|
|
186
|
+
throw new Error(`Missing capacity acceptance configuration: ${config.missing.join(", ")}.`);
|
|
187
|
+
}
|
|
188
|
+
const runnerId = `${input.prefix}-runner`;
|
|
189
|
+
const assignmentId = `${input.prefix}-assignment`;
|
|
190
|
+
const mode = configuredValue(input.env, ["TREESEED_CAPACITY_ACCEPTANCE_MODE"]) || "planning";
|
|
191
|
+
const metadata = {
|
|
192
|
+
liveAcceptance: true,
|
|
193
|
+
runId: input.runId,
|
|
194
|
+
prefix: input.prefix,
|
|
195
|
+
provider: input.provider,
|
|
196
|
+
capability: input.provider === "railway" ? "capacity-provider-runtime-assignment-proof" : "capacity-provider-assignment-proof",
|
|
197
|
+
priority: 1e6
|
|
198
|
+
};
|
|
199
|
+
const adminClient = new MarketClient({
|
|
200
|
+
profile: {
|
|
201
|
+
id: "capacity-acceptance",
|
|
202
|
+
label: "Capacity Acceptance",
|
|
203
|
+
baseUrl: config.apiUrl,
|
|
204
|
+
kind: "specialized",
|
|
205
|
+
teamId: config.teamId
|
|
206
|
+
},
|
|
207
|
+
accessToken: config.adminToken,
|
|
208
|
+
fetchImpl: input.fetchImpl,
|
|
209
|
+
userAgent: `treeseed-live-acceptance/${input.runId}`
|
|
210
|
+
});
|
|
211
|
+
const providerClient = new MarketProviderClient({
|
|
212
|
+
marketUrl: config.apiUrl,
|
|
213
|
+
marketId: input.environment,
|
|
214
|
+
apiKey: config.providerApiKey,
|
|
215
|
+
fetchImpl: input.fetchImpl,
|
|
216
|
+
userAgent: `treeseed-live-acceptance/${input.runId}`
|
|
217
|
+
});
|
|
218
|
+
const existingClasses = await adminClient.projectAgentClasses(config.projectId).catch(() => ({ payload: [] }));
|
|
219
|
+
const hasAgentClass = Array.isArray(existingClasses.payload) && existingClasses.payload.some((entry) => {
|
|
220
|
+
const record = entry && typeof entry === "object" ? entry : {};
|
|
221
|
+
return record.id === config.agentClassId || record.slug === config.agentClassId;
|
|
222
|
+
});
|
|
223
|
+
if (!hasAgentClass) {
|
|
224
|
+
await adminClient.createProjectAgentClass(config.projectId, {
|
|
225
|
+
id: config.agentClassId,
|
|
226
|
+
slug: config.agentClassId,
|
|
227
|
+
name: "Planning",
|
|
228
|
+
status: "active",
|
|
229
|
+
allowedModes: ["planning"],
|
|
230
|
+
requiredCapabilities: ["agent_mode_run"],
|
|
231
|
+
metadata
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const grants = await adminClient.capacityGrants(config.teamId).catch(() => ({ payload: [] }));
|
|
235
|
+
const acceptanceGrant = capacityGrantForAcceptance(Array.isArray(grants.payload) ? grants.payload : [], config, input.environment);
|
|
236
|
+
if (!acceptanceGrant) {
|
|
237
|
+
throw new Error(`Capacity acceptance did not find an active checked-in grant for project ${config.projectId} and provider ${config.providerId}.`);
|
|
238
|
+
}
|
|
239
|
+
const reservation = await adminClient.createCapacityReservation(config.projectId, {
|
|
240
|
+
id: `${input.prefix}-reservation`,
|
|
241
|
+
capacityProviderId: config.providerId,
|
|
242
|
+
laneId: typeof acceptanceGrant.laneId === "string" && acceptanceGrant.laneId ? acceptanceGrant.laneId : `${config.providerId}:agent-capacity`,
|
|
243
|
+
reservedCredits: 1,
|
|
244
|
+
nativeUnit: "request",
|
|
245
|
+
reservedNativeAmount: 1,
|
|
246
|
+
mode,
|
|
247
|
+
metadata
|
|
248
|
+
});
|
|
249
|
+
const reservationId = String(reservation.payload?.id ?? "");
|
|
250
|
+
if (!reservationId) throw new Error("Capacity acceptance reservation creation did not return an id.");
|
|
251
|
+
const checkIn = await providerClient.checkIn({
|
|
252
|
+
id: `${input.prefix}-session`,
|
|
253
|
+
environment: input.environment,
|
|
254
|
+
status: "open",
|
|
255
|
+
capabilities: ["repo_read", "agent_mode_run", "usage_report"],
|
|
256
|
+
grants: [{
|
|
257
|
+
grantId: String(acceptanceGrant.id),
|
|
258
|
+
projectId: config.projectId,
|
|
259
|
+
teamId: config.teamId,
|
|
260
|
+
grantScope: typeof acceptanceGrant.grantScope === "string" ? acceptanceGrant.grantScope : "project"
|
|
261
|
+
}],
|
|
262
|
+
nativeLimits: { wallMinutes: { session: 30 } },
|
|
263
|
+
runnerPressure: { activeRunners: 0, maxConcurrentRunners: 1 },
|
|
264
|
+
constraints: { liveAcceptance: true, outboundOnly: true },
|
|
265
|
+
metadata
|
|
266
|
+
});
|
|
267
|
+
const sessionId = String(checkIn.payload.id ?? "");
|
|
268
|
+
if (!sessionId) throw new Error("Capacity acceptance check-in did not return a session id.");
|
|
269
|
+
await adminClient.createProviderAssignment(config.teamId, {
|
|
270
|
+
id: assignmentId,
|
|
271
|
+
projectId: config.projectId,
|
|
272
|
+
capacityProviderId: config.providerId,
|
|
273
|
+
providerSessionId: sessionId,
|
|
274
|
+
projectAgentClassId: config.agentClassId,
|
|
275
|
+
reservationId,
|
|
276
|
+
mode,
|
|
277
|
+
capacityEnvelope: {
|
|
278
|
+
teamId: config.teamId,
|
|
279
|
+
projectId: config.projectId,
|
|
280
|
+
providerId: config.providerId,
|
|
281
|
+
laneId: typeof acceptanceGrant.laneId === "string" && acceptanceGrant.laneId ? acceptanceGrant.laneId : `${config.providerId}:agent-capacity`,
|
|
282
|
+
reservationId,
|
|
283
|
+
mode,
|
|
284
|
+
limits: { wallMinutes: 5 },
|
|
285
|
+
metadata
|
|
286
|
+
},
|
|
287
|
+
decisionInput: {
|
|
288
|
+
kind: "capacity_acceptance_diagnostic",
|
|
289
|
+
runId: input.runId,
|
|
290
|
+
mode,
|
|
291
|
+
instructions: "Execute the isolated capacity acceptance diagnostic without widening project or provider scope."
|
|
292
|
+
},
|
|
293
|
+
workspaceContext: {
|
|
294
|
+
liveAcceptance: true,
|
|
295
|
+
runId: input.runId
|
|
296
|
+
},
|
|
297
|
+
metadata
|
|
298
|
+
});
|
|
299
|
+
let leased = null;
|
|
300
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
301
|
+
leased = await providerClient.nextAssignment({
|
|
302
|
+
sessionId,
|
|
303
|
+
runnerId,
|
|
304
|
+
leaseSeconds: 120,
|
|
305
|
+
metadata
|
|
306
|
+
});
|
|
307
|
+
const leasedId = leased.payload?.id ? String(leased.payload.id) : "";
|
|
308
|
+
if (!leasedId) throw new Error("Capacity acceptance did not lease an assignment.");
|
|
309
|
+
if (leasedId === assignmentId) break;
|
|
310
|
+
const leasedMetadata = leased.payload?.metadata && typeof leased.payload.metadata === "object" ? leased.payload.metadata : {};
|
|
311
|
+
if (leasedMetadata.liveAcceptance !== true) {
|
|
312
|
+
throw new Error(`Capacity acceptance leased ${leasedId} instead of diagnostic assignment ${assignmentId}.`);
|
|
313
|
+
}
|
|
314
|
+
const staleLeaseToken = leased.leaseToken ?? leased.payload.leaseToken ?? null;
|
|
315
|
+
if (!staleLeaseToken) throw new Error(`Capacity acceptance leased stale diagnostic ${leasedId} without a lease token.`);
|
|
316
|
+
await providerClient.completeAssignment(leasedId, {
|
|
317
|
+
runnerId,
|
|
318
|
+
leaseToken: staleLeaseToken,
|
|
319
|
+
output: { summary: "Retired stale capacity acceptance diagnostic.", runId: input.runId },
|
|
320
|
+
summary: { runId: input.runId, retiredAssignmentId: leasedId },
|
|
321
|
+
metadata: {
|
|
322
|
+
...metadata,
|
|
323
|
+
retiredStaleAcceptanceAssignment: true,
|
|
324
|
+
retiredAssignmentId: leasedId
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
leased = null;
|
|
328
|
+
}
|
|
329
|
+
if (!leased?.payload?.id) throw new Error("Capacity acceptance did not lease an assignment.");
|
|
330
|
+
if (leased.payload.id !== assignmentId) {
|
|
331
|
+
throw new Error(`Capacity acceptance leased stale diagnostics but did not reach diagnostic assignment ${assignmentId}.`);
|
|
332
|
+
}
|
|
333
|
+
const leaseToken = leased.leaseToken ?? leased.payload.leaseToken ?? null;
|
|
334
|
+
if (!leaseToken) throw new Error("Capacity acceptance lease did not include a lease token.");
|
|
335
|
+
await createTreeDxProxyAuditEvidence({
|
|
336
|
+
fetchImpl: input.fetchImpl,
|
|
337
|
+
apiUrl: config.apiUrl,
|
|
338
|
+
providerApiKey: config.providerApiKey,
|
|
339
|
+
projectId: config.projectId,
|
|
340
|
+
assignmentId,
|
|
341
|
+
runId: input.runId
|
|
342
|
+
});
|
|
343
|
+
const modeRun = await providerClient.createAssignmentModeRun(assignmentId, {
|
|
344
|
+
status: "succeeded",
|
|
345
|
+
selectedInput: { kind: "capacity_acceptance_diagnostic", runId: input.runId, mode },
|
|
346
|
+
outputs: { summary: "Capacity acceptance diagnostic completed.", runId: input.runId },
|
|
347
|
+
usageActual: {
|
|
348
|
+
actualCredits: 0,
|
|
349
|
+
nativeUsage: { nativeUnit: "request", amount: 1, source: "capacity_acceptance" },
|
|
350
|
+
metadata
|
|
351
|
+
},
|
|
352
|
+
traceRefs: { liveAcceptanceRunId: input.runId },
|
|
353
|
+
metadata
|
|
354
|
+
});
|
|
355
|
+
const modeRunId = String(modeRun.payload.id ?? "");
|
|
356
|
+
if (!modeRunId) throw new Error("Capacity acceptance mode-run creation did not return an id.");
|
|
357
|
+
const completed = await providerClient.completeAssignment(assignmentId, {
|
|
358
|
+
runnerId,
|
|
359
|
+
leaseToken,
|
|
360
|
+
modeRunId,
|
|
361
|
+
actualCredits: 0.25,
|
|
362
|
+
nativeUsage: { nativeUnit: "request", amount: 1, source: "capacity_acceptance" },
|
|
363
|
+
output: { summary: "Capacity acceptance diagnostic completed.", runId: input.runId },
|
|
364
|
+
summary: { runId: input.runId, modeRunId },
|
|
365
|
+
metadata
|
|
366
|
+
});
|
|
367
|
+
const finalStatus = String(completed.payload?.status ?? "");
|
|
368
|
+
if (finalStatus !== "completed") {
|
|
369
|
+
throw new Error(`Capacity acceptance assignment finished with status "${finalStatus || "unknown"}".`);
|
|
370
|
+
}
|
|
371
|
+
const modeRuns = await adminClient.projectAgentModeRuns(config.projectId, { assignmentId });
|
|
372
|
+
const modeRunCount = Array.isArray(modeRuns.payload) ? modeRuns.payload.length : 0;
|
|
373
|
+
if (!modeRunCount) throw new Error("Capacity acceptance mode-run was not visible through project mode-run inspection.");
|
|
374
|
+
return {
|
|
375
|
+
sessionId,
|
|
376
|
+
assignmentId,
|
|
377
|
+
modeRunId,
|
|
378
|
+
finalStatus,
|
|
379
|
+
mode,
|
|
380
|
+
runnerId,
|
|
381
|
+
modeRunCount
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function shortRunId(now = /* @__PURE__ */ new Date()) {
|
|
385
|
+
return now.toISOString().replace(/[^0-9]/gu, "").slice(0, 14);
|
|
386
|
+
}
|
|
387
|
+
function providerPrefix(environment, provider, runId) {
|
|
388
|
+
if (provider === "railway") return `trsd-rail-${runId}`.toLowerCase();
|
|
389
|
+
return `trsd-live-${environment}-${provider}-${runId}`.toLowerCase();
|
|
390
|
+
}
|
|
391
|
+
function providerPrefixRoot(environment, provider) {
|
|
392
|
+
if (provider === "railway") return "trsd-rail-";
|
|
393
|
+
return `trsd-live-${environment}-${provider}-`.toLowerCase();
|
|
394
|
+
}
|
|
395
|
+
function emitProgress(onProgress, event) {
|
|
396
|
+
if (!onProgress) return;
|
|
397
|
+
onProgress({
|
|
398
|
+
...event,
|
|
399
|
+
message: event.message ?? [
|
|
400
|
+
event.provider,
|
|
401
|
+
event.capability,
|
|
402
|
+
event.phase
|
|
403
|
+
].filter(Boolean).join(":")
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function sleep(ms) {
|
|
407
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
408
|
+
}
|
|
409
|
+
async function waitForLiveObservation(description, observe, isReady, options = {}) {
|
|
410
|
+
const attempts = Math.max(1, options.attempts ?? 8);
|
|
411
|
+
const intervalMs = Math.max(0, options.intervalMs ?? 750);
|
|
412
|
+
let lastValue;
|
|
413
|
+
let lastError;
|
|
414
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
415
|
+
try {
|
|
416
|
+
lastValue = await observe();
|
|
417
|
+
if (isReady(lastValue)) return lastValue;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
lastError = error;
|
|
420
|
+
}
|
|
421
|
+
if (attempt < attempts) await sleep(intervalMs);
|
|
422
|
+
}
|
|
423
|
+
if (lastError) {
|
|
424
|
+
throw new Error(`${description} was not observed live: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`${description} was not observed live after ${attempts} attempt(s).`);
|
|
427
|
+
}
|
|
428
|
+
function parseGitHubRepository(value) {
|
|
429
|
+
const raw = value.trim().replace(/^https?:\/\/github\.com\//iu, "").replace(/^git@github\.com:/iu, "").replace(/^ssh:\/\/git@github\.com\//iu, "").replace(/\.git$/iu, "").replace(/^\/+|\/+$/gu, "");
|
|
430
|
+
const [owner, repo, ...extra] = raw.split("/").filter(Boolean);
|
|
431
|
+
if (!owner || !repo || extra.length > 0) {
|
|
432
|
+
throw new Error(`Invalid GitHub repository "${value}". Expected owner/name.`);
|
|
433
|
+
}
|
|
434
|
+
return `${owner}/${repo}`;
|
|
435
|
+
}
|
|
436
|
+
function resolveCurrentGitHubRepository(cwd, env) {
|
|
437
|
+
const configured = configuredValue(env, ["TREESEED_REPOSITORY", "GITHUB_REPOSITORY"]);
|
|
438
|
+
if (configured) return parseGitHubRepository(configured);
|
|
439
|
+
const remote = runTreeseedGit(["config", "--get", "remote.origin.url"], {
|
|
440
|
+
cwd,
|
|
441
|
+
mode: "read"
|
|
442
|
+
}).stdout.trim();
|
|
443
|
+
return parseGitHubRepository(remote);
|
|
444
|
+
}
|
|
445
|
+
function domainFromWorkspace(cwd) {
|
|
446
|
+
try {
|
|
447
|
+
const manifest = readFileSync(join(cwd, "treeseed.site.yaml"), "utf8");
|
|
448
|
+
const match = /^siteUrl:\s*(\S+)/gmu.exec(manifest);
|
|
449
|
+
if (!match?.[1]) return "";
|
|
450
|
+
const url = new URL(match[1]);
|
|
451
|
+
return url.hostname.replace(/^www\./iu, "");
|
|
452
|
+
} catch {
|
|
453
|
+
return "";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function resolveLiveTestDomain(cwd, env) {
|
|
457
|
+
return configuredValue(env, ["TREESEED_LIVE_TEST_DOMAIN"]) || configuredValue(env, ["TREESEED_DOMAIN"]) || domainFromWorkspace(cwd);
|
|
458
|
+
}
|
|
459
|
+
async function resolveCloudflareZoneId(domain, env, fetchImpl) {
|
|
460
|
+
const configured = configuredValue(env, ["TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID", "CLOUDFLARE_ZONE_ID"]);
|
|
461
|
+
if (configured) return configured;
|
|
462
|
+
const zones = await cloudflareRequest("/zones?per_page=100", env, fetchImpl).catch(() => []);
|
|
463
|
+
if (!Array.isArray(zones)) return "";
|
|
464
|
+
const candidates = zones.map((entry) => entry && typeof entry === "object" ? entry : null).filter(Boolean);
|
|
465
|
+
const matched = candidates.find((entry) => typeof entry.name === "string" && (domain === entry.name || domain.endsWith(`.${entry.name}`)));
|
|
466
|
+
return typeof matched?.id === "string" ? matched.id : "";
|
|
467
|
+
}
|
|
468
|
+
function scenario({
|
|
469
|
+
provider,
|
|
470
|
+
mode,
|
|
471
|
+
prefix,
|
|
472
|
+
capability,
|
|
473
|
+
ok,
|
|
474
|
+
phase,
|
|
475
|
+
action,
|
|
476
|
+
reason,
|
|
477
|
+
locators = {},
|
|
478
|
+
createdResources = [],
|
|
479
|
+
updatedResources = [],
|
|
480
|
+
replacedResources = [],
|
|
481
|
+
destroyedResources = [],
|
|
482
|
+
retainedResources = [],
|
|
483
|
+
issues = [],
|
|
484
|
+
startedAt,
|
|
485
|
+
completedAt,
|
|
486
|
+
durationMs
|
|
487
|
+
}) {
|
|
488
|
+
const completed = completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
+
const started = startedAt ?? completed;
|
|
490
|
+
return {
|
|
491
|
+
id: `live-test:${provider}:${capability}`,
|
|
492
|
+
provider,
|
|
493
|
+
capability,
|
|
494
|
+
mode,
|
|
495
|
+
ok,
|
|
496
|
+
phase,
|
|
497
|
+
action,
|
|
498
|
+
reason,
|
|
499
|
+
startedAt: started,
|
|
500
|
+
completedAt: completed,
|
|
501
|
+
durationMs: typeof durationMs === "number" ? Math.max(1, Math.ceil(durationMs)) : Math.max(1, Date.parse(completed) - Date.parse(started)),
|
|
502
|
+
locators,
|
|
503
|
+
createdResources,
|
|
504
|
+
updatedResources,
|
|
505
|
+
replacedResources,
|
|
506
|
+
destroyedResources,
|
|
507
|
+
retainedResources,
|
|
508
|
+
issues
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function node(provider, environment, type, id, state = {}) {
|
|
512
|
+
return {
|
|
513
|
+
id,
|
|
514
|
+
provider,
|
|
515
|
+
type,
|
|
516
|
+
owner: "reconcile-live-test",
|
|
517
|
+
environment,
|
|
518
|
+
state
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function redactProviderState(value) {
|
|
522
|
+
if (Array.isArray(value)) return value.map((entry) => redactProviderState(entry));
|
|
523
|
+
if (value && typeof value === "object") {
|
|
524
|
+
const redacted = {};
|
|
525
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
526
|
+
redacted[key] = /secret|token|password|private|key/iu.test(key) ? "[redacted]" : redactProviderState(entry);
|
|
527
|
+
}
|
|
528
|
+
return redacted;
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
function providerNode(provider, environment, type, id, state = {}) {
|
|
533
|
+
return node(provider, environment, type, id, redactProviderState(state));
|
|
534
|
+
}
|
|
535
|
+
function blocking(provider, type, reason) {
|
|
536
|
+
return {
|
|
537
|
+
id: `live-test:${provider}:${type}:blocked`,
|
|
538
|
+
resourceId: `live-test:${provider}:${type}`,
|
|
539
|
+
severity: "blocking",
|
|
540
|
+
reason,
|
|
541
|
+
provider,
|
|
542
|
+
type
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function measuredScenario(input, fn) {
|
|
546
|
+
const started = /* @__PURE__ */ new Date();
|
|
547
|
+
const startedMs = performance.now();
|
|
548
|
+
emitProgress(input.onProgress, {
|
|
549
|
+
provider: input.provider,
|
|
550
|
+
mode: input.mode,
|
|
551
|
+
environment: input.environment,
|
|
552
|
+
runId: input.runId,
|
|
553
|
+
resourcePrefix: input.prefix,
|
|
554
|
+
capability: input.capability,
|
|
555
|
+
phase: input.phase === "verify" ? "verify" : input.phase === "cleanup" ? "cleanup" : input.phase === "destroy" ? "destroy" : "create",
|
|
556
|
+
message: input.startMessage ?? `${input.provider}:${input.capability}: ${input.phase} started`
|
|
557
|
+
});
|
|
558
|
+
try {
|
|
559
|
+
const value = await fn();
|
|
560
|
+
const completed = /* @__PURE__ */ new Date();
|
|
561
|
+
const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
|
|
562
|
+
const resourcesFor = (resources) => typeof resources === "function" ? resources(value) : resources ?? [];
|
|
563
|
+
emitProgress(input.onProgress, {
|
|
564
|
+
provider: input.provider,
|
|
565
|
+
mode: input.mode,
|
|
566
|
+
environment: input.environment,
|
|
567
|
+
runId: input.runId,
|
|
568
|
+
resourcePrefix: input.prefix,
|
|
569
|
+
capability: input.capability,
|
|
570
|
+
phase: "complete",
|
|
571
|
+
elapsedMs: durationMs,
|
|
572
|
+
message: `${input.provider}:${input.capability}: ok in ${durationMs}ms`
|
|
573
|
+
});
|
|
574
|
+
return scenario({
|
|
575
|
+
provider: input.provider,
|
|
576
|
+
mode: input.mode,
|
|
577
|
+
prefix: input.prefix,
|
|
578
|
+
capability: input.capability,
|
|
579
|
+
ok: true,
|
|
580
|
+
phase: input.phase,
|
|
581
|
+
action: input.action,
|
|
582
|
+
reason: typeof input.successReason === "function" ? input.successReason(value) : input.successReason,
|
|
583
|
+
locators: input.locators,
|
|
584
|
+
createdResources: resourcesFor(input.createdResources),
|
|
585
|
+
updatedResources: resourcesFor(input.updatedResources),
|
|
586
|
+
replacedResources: resourcesFor(input.replacedResources),
|
|
587
|
+
destroyedResources: resourcesFor(input.destroyedResources),
|
|
588
|
+
retainedResources: resourcesFor(input.retainedResources),
|
|
589
|
+
startedAt: started.toISOString(),
|
|
590
|
+
completedAt: completed.toISOString(),
|
|
591
|
+
durationMs
|
|
592
|
+
});
|
|
593
|
+
} catch (error) {
|
|
594
|
+
const completed = /* @__PURE__ */ new Date();
|
|
595
|
+
const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
|
|
596
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
597
|
+
emitProgress(input.onProgress, {
|
|
598
|
+
provider: input.provider,
|
|
599
|
+
mode: input.mode,
|
|
600
|
+
environment: input.environment,
|
|
601
|
+
runId: input.runId,
|
|
602
|
+
resourcePrefix: input.prefix,
|
|
603
|
+
capability: input.capability,
|
|
604
|
+
phase: "blocked",
|
|
605
|
+
elapsedMs: durationMs,
|
|
606
|
+
message: `${input.provider}:${input.capability}: blocked after ${durationMs}ms - ${reason}`
|
|
607
|
+
});
|
|
608
|
+
return scenario({
|
|
609
|
+
provider: input.provider,
|
|
610
|
+
mode: input.mode,
|
|
611
|
+
prefix: input.prefix,
|
|
612
|
+
capability: input.capability,
|
|
613
|
+
ok: false,
|
|
614
|
+
phase: "blocked",
|
|
615
|
+
action: "blocked",
|
|
616
|
+
reason,
|
|
617
|
+
locators: input.locators,
|
|
618
|
+
startedAt: started.toISOString(),
|
|
619
|
+
completedAt: completed.toISOString(),
|
|
620
|
+
durationMs
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function reportForProvider({
|
|
625
|
+
provider,
|
|
626
|
+
mode,
|
|
627
|
+
runId,
|
|
628
|
+
prefix,
|
|
629
|
+
environment,
|
|
630
|
+
results,
|
|
631
|
+
cleanupDrift = []
|
|
632
|
+
}) {
|
|
633
|
+
const capabilities = PROVIDER_CAPABILITIES[provider];
|
|
634
|
+
const desiredGraph = capabilities.map((capability) => ({
|
|
635
|
+
id: `live-test:${provider}:${capability}`,
|
|
636
|
+
provider,
|
|
637
|
+
type: capability,
|
|
638
|
+
owner: "reconcile-live-test",
|
|
639
|
+
environment,
|
|
640
|
+
spec: { prefix, isolated: true, mode }
|
|
641
|
+
}));
|
|
642
|
+
const resultByCapability = new Map(results.map((result) => [result.capability, result]));
|
|
643
|
+
const blockedDrift = [
|
|
644
|
+
...desiredGraph.filter((entry) => !resultByCapability.get(String(entry.type))?.ok).map((entry) => blocking(provider, String(entry.type), resultByCapability.get(String(entry.type))?.reason ?? "Live scenario did not run.")),
|
|
645
|
+
...cleanupDrift
|
|
646
|
+
];
|
|
647
|
+
const actions = desiredGraph.map((entry) => {
|
|
648
|
+
const result = resultByCapability.get(String(entry.type));
|
|
649
|
+
return {
|
|
650
|
+
id: `${entry.id}:${result?.action ?? "blocked"}`,
|
|
651
|
+
kind: result?.ok ? result.action : "blocked",
|
|
652
|
+
resourceId: entry.id,
|
|
653
|
+
reason: result?.reason ?? "Live scenario did not run.",
|
|
654
|
+
provider,
|
|
655
|
+
type: entry.type
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
const postconditions = desiredGraph.map((entry) => {
|
|
659
|
+
const result = resultByCapability.get(String(entry.type));
|
|
660
|
+
return {
|
|
661
|
+
id: `${entry.id}:postcondition`,
|
|
662
|
+
resourceId: entry.id,
|
|
663
|
+
description: `${mode} live reconciliation postconditions pass for ${provider}:${entry.type}.`,
|
|
664
|
+
source: provider === "local" ? "local" : "api",
|
|
665
|
+
required: true,
|
|
666
|
+
ok: Boolean(result?.ok),
|
|
667
|
+
issues: result?.ok ? [] : [result?.reason ?? "Live scenario did not run."],
|
|
668
|
+
observed: result?.locators ?? {}
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
const createdResources = results.flatMap((result) => result.createdResources);
|
|
672
|
+
const updatedResources = results.flatMap((result) => result.updatedResources);
|
|
673
|
+
const replacedResources = results.flatMap((result) => result.replacedResources);
|
|
674
|
+
const destroyedResources = results.flatMap((result) => result.destroyedResources);
|
|
675
|
+
const retainedResources = results.flatMap((result) => result.retainedResources);
|
|
676
|
+
const report = createTreeseedCanonicalReconcileReport({
|
|
677
|
+
desiredGraph,
|
|
678
|
+
observedGraph: desiredGraph.filter((entry) => resultByCapability.has(String(entry.type))).map((entry) => ({
|
|
679
|
+
...entry,
|
|
680
|
+
state: {
|
|
681
|
+
verified: Boolean(resultByCapability.get(String(entry.type))?.ok),
|
|
682
|
+
locators: resultByCapability.get(String(entry.type))?.locators ?? {}
|
|
683
|
+
}
|
|
684
|
+
})),
|
|
685
|
+
diff: blockedDrift,
|
|
686
|
+
actions,
|
|
687
|
+
postconditions,
|
|
688
|
+
blockedDrift,
|
|
689
|
+
retainedResources,
|
|
690
|
+
destroyedResources,
|
|
691
|
+
liveVerification: {
|
|
692
|
+
ok: blockedDrift.length === 0,
|
|
693
|
+
source: `reconcile-live-test:${mode}`,
|
|
694
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
695
|
+
issues: blockedDrift.map((entry) => entry.reason)
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
provider,
|
|
700
|
+
mode,
|
|
701
|
+
runId,
|
|
702
|
+
resourcePrefix: prefix,
|
|
703
|
+
scenarioResults: results,
|
|
704
|
+
coverage: {
|
|
705
|
+
total: capabilities.length,
|
|
706
|
+
passed: results.filter((result) => result.ok).length,
|
|
707
|
+
failed: capabilities.length - results.filter((result) => result.ok).length,
|
|
708
|
+
capabilities
|
|
709
|
+
},
|
|
710
|
+
createdResources,
|
|
711
|
+
updatedResources,
|
|
712
|
+
replacedResources,
|
|
713
|
+
destroyedResources,
|
|
714
|
+
retainedResources,
|
|
715
|
+
cleanupDrift,
|
|
716
|
+
report,
|
|
717
|
+
ok: report.ok
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async function cloudflareRequestPayload(path, env, fetchImpl, init = {}) {
|
|
721
|
+
const token = configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]);
|
|
722
|
+
if (!token) throw new Error("Missing TREESEED_CLOUDFLARE_API_TOKEN.");
|
|
723
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
|
|
724
|
+
...init,
|
|
725
|
+
headers: {
|
|
726
|
+
Accept: "application/json",
|
|
727
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
728
|
+
Authorization: `Bearer ${token}`,
|
|
729
|
+
...init.headers ?? {}
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
const payload = await response.json().catch(() => ({}));
|
|
733
|
+
if (!response.ok || payload.success === false) {
|
|
734
|
+
const errors = Array.isArray(payload.errors) ? payload.errors.map((entry) => entry.message).filter(Boolean).join("; ") : "";
|
|
735
|
+
throw new Error(`${response.status} ${response.statusText}${errors ? `: ${errors}` : ""}`);
|
|
736
|
+
}
|
|
737
|
+
return payload;
|
|
738
|
+
}
|
|
739
|
+
async function cloudflareRequest(path, env, fetchImpl, init = {}) {
|
|
740
|
+
const payload = await cloudflareRequestPayload(path, env, fetchImpl, init);
|
|
741
|
+
return payload.result;
|
|
742
|
+
}
|
|
743
|
+
function isTransientCloudflareError(error) {
|
|
744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
745
|
+
return /\b(500|502|503|504|520|521|522|523|524)\b|internal server error|unknown error occurred|temporar(?:y|ily)|timeout|rate limit/iu.test(message);
|
|
746
|
+
}
|
|
747
|
+
async function withCloudflareTransientRetry(operation, options = {}) {
|
|
748
|
+
const attempts = Math.max(1, options.attempts ?? 4);
|
|
749
|
+
const delayMs = Math.max(0, options.delayMs ?? 1500);
|
|
750
|
+
let lastError;
|
|
751
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
752
|
+
try {
|
|
753
|
+
return await operation();
|
|
754
|
+
} catch (error) {
|
|
755
|
+
lastError = error;
|
|
756
|
+
if (!isTransientCloudflareError(error) || attempt >= attempts) break;
|
|
757
|
+
await sleep(delayMs * attempt);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
761
|
+
}
|
|
762
|
+
async function cloudflareRawRequest(path, env, fetchImpl, init = {}) {
|
|
763
|
+
const token = configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]);
|
|
764
|
+
if (!token) throw new Error("Missing TREESEED_CLOUDFLARE_API_TOKEN.");
|
|
765
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
|
|
766
|
+
...init,
|
|
767
|
+
headers: {
|
|
768
|
+
Accept: "*/*",
|
|
769
|
+
Authorization: `Bearer ${token}`,
|
|
770
|
+
...init.headers ?? {}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
const body = await response.text().catch(() => "");
|
|
774
|
+
if (!response.ok) {
|
|
775
|
+
throw new Error(`${response.status} ${response.statusText}${body ? `: ${body.slice(0, 300)}` : ""}`);
|
|
776
|
+
}
|
|
777
|
+
return body;
|
|
778
|
+
}
|
|
779
|
+
async function githubRequest(path, token, fetchImpl, init = {}) {
|
|
780
|
+
const response = await fetchImpl(`https://api.github.com${path}`, {
|
|
781
|
+
...init,
|
|
782
|
+
headers: {
|
|
783
|
+
Accept: "application/vnd.github+json",
|
|
784
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
785
|
+
Authorization: `Bearer ${token}`,
|
|
786
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
787
|
+
...init.headers ?? {}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
const payload = await response.json().catch(() => ({}));
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
throw new Error(`${response.status} ${response.statusText}${payload.message ? `: ${payload.message}` : ""}`);
|
|
793
|
+
}
|
|
794
|
+
return payload;
|
|
795
|
+
}
|
|
796
|
+
async function runSmokeProvider({
|
|
797
|
+
provider,
|
|
798
|
+
environment,
|
|
799
|
+
prefix,
|
|
800
|
+
mode,
|
|
801
|
+
cwd,
|
|
802
|
+
env,
|
|
803
|
+
fetchImpl
|
|
804
|
+
}) {
|
|
805
|
+
if (provider === "railway") {
|
|
806
|
+
if (!configuredValue(env, ["TREESEED_RAILWAY_API_TOKEN"])) {
|
|
807
|
+
return PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
|
|
808
|
+
provider,
|
|
809
|
+
mode,
|
|
810
|
+
prefix,
|
|
811
|
+
capability,
|
|
812
|
+
ok: false,
|
|
813
|
+
phase: "blocked",
|
|
814
|
+
action: "blocked",
|
|
815
|
+
reason: "Missing TREESEED_RAILWAY_API_TOKEN for Railway live reconciliation tests."
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
|
|
820
|
+
const projects = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
|
|
821
|
+
const apiProject = projects.find((project) => project.name === "treeseed-api") ?? projects[0] ?? null;
|
|
822
|
+
const environments = apiProject ? await listRailwayEnvironments({ projectId: apiProject.id, env, fetchImpl }) : [];
|
|
823
|
+
const selectedEnvironment = environments.find((candidate) => candidate.name === environment) ?? environments.find((candidate) => candidate.name === (environment === "prod" ? "production" : "staging")) ?? environments[0] ?? null;
|
|
824
|
+
const services = apiProject ? await listRailwayServices({ projectId: apiProject.id, env, fetchImpl }) : [];
|
|
825
|
+
const variables = apiProject && selectedEnvironment ? await listRailwayVariables({ projectId: apiProject.id, environmentId: selectedEnvironment.id, env, fetchImpl }).catch(() => ({})) : {};
|
|
826
|
+
const volumes = apiProject ? await listRailwayVolumes({ projectId: apiProject.id, env, fetchImpl }).catch(() => []) : [];
|
|
827
|
+
const serviceNames = services.map((service) => service.name);
|
|
828
|
+
const base = {
|
|
829
|
+
workspaceId: workspace.id,
|
|
830
|
+
projectId: apiProject?.id ?? null,
|
|
831
|
+
environmentId: selectedEnvironment?.id ?? null
|
|
832
|
+
};
|
|
833
|
+
return [
|
|
834
|
+
scenario({ provider, mode, prefix, capability: "project", ok: projects.length > 0, phase: "smoke", action: "noop", reason: projects.length ? "Railway projects are observable." : "No Railway projects are visible.", locators: base }),
|
|
835
|
+
scenario({ provider, mode, prefix, capability: "environment", ok: Boolean(selectedEnvironment), phase: "smoke", action: "noop", reason: selectedEnvironment ? "Railway environments are observable." : "No Railway environment is visible.", locators: base }),
|
|
836
|
+
scenario({ provider, mode, prefix, capability: "service", ok: services.length > 0, phase: "smoke", action: "noop", reason: services.length ? "Railway services are observable." : "No Railway services are visible.", locators: base }),
|
|
837
|
+
scenario({ provider, mode, prefix, capability: "image-service", ok: serviceNames.some((name) => /api|runner|treedx/iu.test(name)), phase: "smoke", action: "noop", reason: "Railway image service observation completed.", locators: base }),
|
|
838
|
+
scenario({ provider, mode, prefix, capability: "postgres", ok: serviceNames.some((name) => /postgres/iu.test(name)), phase: "smoke", action: "noop", reason: "Railway PostgreSQL observation completed.", locators: base }),
|
|
839
|
+
scenario({ provider, mode, prefix, capability: "volume", ok: volumes.length > 0, phase: "smoke", action: "noop", reason: "Railway volume observation completed.", locators: base }),
|
|
840
|
+
scenario({ provider, mode, prefix, capability: "domain", ok: true, phase: "smoke", action: "noop", reason: "Railway domain API uses authenticated provider surface.", locators: base }),
|
|
841
|
+
scenario({ provider, mode, prefix, capability: "variables", ok: Boolean(selectedEnvironment), phase: "smoke", action: "noop", reason: `Railway variables API observed ${Object.keys(variables).length} variables.`, locators: base }),
|
|
842
|
+
scenario({ provider, mode, prefix, capability: "deployment-health", ok: services.length > 0, phase: "smoke", action: "noop", reason: "Railway deployment-health inspection has observable services.", locators: base }),
|
|
843
|
+
scenario({ provider, mode, prefix, capability: "capacity-provider-runtime-assignment-proof", ok: true, phase: "smoke", action: "noop", reason: "Railway capacity runtime proof is available through explicit acceptance mode.", locators: base })
|
|
844
|
+
];
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
847
|
+
return PROVIDER_CAPABILITIES.railway.map((capability) => scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason }));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (provider === "cloudflare") {
|
|
851
|
+
const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
|
|
852
|
+
if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) || !accountId) {
|
|
853
|
+
return PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({
|
|
854
|
+
provider,
|
|
855
|
+
mode,
|
|
856
|
+
prefix,
|
|
857
|
+
capability,
|
|
858
|
+
ok: false,
|
|
859
|
+
phase: "blocked",
|
|
860
|
+
action: "blocked",
|
|
861
|
+
reason: "Missing TREESEED_CLOUDFLARE_API_TOKEN or TREESEED_CLOUDFLARE_ACCOUNT_ID for Cloudflare live reconciliation tests."
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
const checks = [
|
|
865
|
+
["pages", `/accounts/${accountId}/pages/projects?per_page=1`],
|
|
866
|
+
["worker", `/accounts/${accountId}/workers/services?per_page=1`],
|
|
867
|
+
["d1", `/accounts/${accountId}/d1/database?per_page=1`],
|
|
868
|
+
["r2", `/accounts/${accountId}/r2/buckets?per_page=1`],
|
|
869
|
+
["kv", `/accounts/${accountId}/storage/kv/namespaces?per_page=1`],
|
|
870
|
+
["queue", `/accounts/${accountId}/queues?per_page=1`],
|
|
871
|
+
["dns", "/zones?per_page=1"],
|
|
872
|
+
["turnstile", `/accounts/${accountId}/challenges/widgets?per_page=1`],
|
|
873
|
+
["secrets", `/accounts/${accountId}/workers/services?per_page=1`],
|
|
874
|
+
["cache-rules", "/zones?per_page=1"]
|
|
875
|
+
];
|
|
876
|
+
return Promise.all(checks.map(async ([capability, path]) => {
|
|
877
|
+
try {
|
|
878
|
+
await cloudflareRequest(path, env, fetchImpl);
|
|
879
|
+
return scenario({ provider, mode, prefix, capability, ok: true, phase: "smoke", action: "noop", reason: "Cloudflare API surface is reachable.", locators: { accountId } });
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: error instanceof Error ? error.message : String(error), locators: { accountId } });
|
|
882
|
+
}
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
if (provider === "github") {
|
|
886
|
+
let repository = "";
|
|
887
|
+
try {
|
|
888
|
+
repository = resolveCurrentGitHubRepository(cwd, env);
|
|
889
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
|
|
890
|
+
if (!credential.token) {
|
|
891
|
+
throw new Error(`Missing GitHub credential for ${repository}; expected ${credential.envName} or TREESEED_GITHUB_TOKEN fallback.`);
|
|
892
|
+
}
|
|
893
|
+
const [owner, repo] = credential.repository.split("/");
|
|
894
|
+
const checks = [
|
|
895
|
+
["environment", `/repos/${owner}/${repo}/environments?per_page=1`],
|
|
896
|
+
["secret", `/repos/${owner}/${repo}/actions/secrets?per_page=1`],
|
|
897
|
+
["variable", `/repos/${owner}/${repo}/actions/variables?per_page=1`],
|
|
898
|
+
["workflow-dispatch", `/repos/${owner}/${repo}/actions/workflows?per_page=1`],
|
|
899
|
+
["workflow-observation", `/repos/${owner}/${repo}/actions/runs?per_page=1`]
|
|
900
|
+
];
|
|
901
|
+
const results = await Promise.all(checks.map(async ([capability, path]) => {
|
|
902
|
+
try {
|
|
903
|
+
await githubRequest(path, credential.token ?? "", fetchImpl);
|
|
904
|
+
return scenario({ provider, mode, prefix, capability, ok: true, phase: "smoke", action: "noop", reason: "GitHub API surface is reachable.", locators: { repository: credential.repository, credentialKey: credential.envName } });
|
|
905
|
+
} catch (error) {
|
|
906
|
+
return scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: error instanceof Error ? error.message : String(error), locators: { repository: credential.repository, credentialKey: credential.envName } });
|
|
907
|
+
}
|
|
908
|
+
}));
|
|
909
|
+
results.push(scenario({ provider, mode, prefix, capability: "repository-scoped-token", ok: true, phase: "smoke", action: "noop", reason: credential.fallbackUsed ? "GitHub credential resolved through fallback." : "GitHub credential resolved through repository-scoped key.", locators: { repository: credential.repository, credentialKey: credential.envName } }));
|
|
910
|
+
return results;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
913
|
+
return PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { repository } }));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return PROVIDER_CAPABILITIES.local.map((capability) => scenario({
|
|
917
|
+
provider,
|
|
918
|
+
mode,
|
|
919
|
+
prefix,
|
|
920
|
+
capability,
|
|
921
|
+
ok: true,
|
|
922
|
+
phase: "smoke",
|
|
923
|
+
action: "noop",
|
|
924
|
+
reason: "Local reconciliation live-test capability is available in-process."
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
async function requireAcceptanceConfig(provider, cwd, env, fetchImpl) {
|
|
928
|
+
const missing = [];
|
|
929
|
+
if (provider === "railway") {
|
|
930
|
+
if (!configuredValue(env, ["TREESEED_RAILWAY_API_TOKEN"])) missing.push("TREESEED_RAILWAY_API_TOKEN");
|
|
931
|
+
if (!resolveLiveTestDomain(cwd, env)) missing.push("TREESEED_LIVE_TEST_DOMAIN or treeseed.site.yaml siteUrl");
|
|
932
|
+
missing.push(...capacityAcceptanceConfig(env).missing);
|
|
933
|
+
}
|
|
934
|
+
if (provider === "cloudflare") {
|
|
935
|
+
if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) missing.push("TREESEED_CLOUDFLARE_API_TOKEN");
|
|
936
|
+
if (!configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"])) missing.push("TREESEED_CLOUDFLARE_ACCOUNT_ID");
|
|
937
|
+
const domain = resolveLiveTestDomain(cwd, env);
|
|
938
|
+
if (!domain) {
|
|
939
|
+
missing.push("TREESEED_LIVE_TEST_DOMAIN or treeseed.site.yaml siteUrl");
|
|
940
|
+
} else if (configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) && !await resolveCloudflareZoneId(domain, env, fetchImpl)) {
|
|
941
|
+
missing.push("TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID or visible Cloudflare zone for live-test domain");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (provider === "github") {
|
|
945
|
+
if (!configuredValue(env, ["TREESEED_GITHUB_TOKEN"])) {
|
|
946
|
+
try {
|
|
947
|
+
const repository = configuredValue(env, ["TREESEED_REPOSITORY", "GITHUB_REPOSITORY"]);
|
|
948
|
+
const credential = repository ? resolveGitHubCredentialForRepository(repository, { values: env, env }) : null;
|
|
949
|
+
if (!credential?.token) missing.push("TREESEED_GITHUB_TOKEN or repository-scoped TREESEED_GITHUB_TOKEN_*");
|
|
950
|
+
} catch {
|
|
951
|
+
missing.push("TREESEED_GITHUB_TOKEN or repository-scoped TREESEED_GITHUB_TOKEN_*");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return missing;
|
|
956
|
+
}
|
|
957
|
+
async function cleanupRailwayPrefixedProjects(environment, env, fetchImpl) {
|
|
958
|
+
const prefixRoot = providerPrefixRoot(environment, "railway");
|
|
959
|
+
const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
|
|
960
|
+
const projects = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
|
|
961
|
+
const prefixed = projects.filter((project) => !project.deletedAt && project.name.startsWith(prefixRoot));
|
|
962
|
+
const destroyed = [];
|
|
963
|
+
for (const project of prefixed) {
|
|
964
|
+
await deleteRailwayProject({ projectId: project.id, env, fetchImpl });
|
|
965
|
+
destroyed.push(node("railway", environment, "project", project.name, { id: project.id, deleted: true }));
|
|
966
|
+
}
|
|
967
|
+
const refreshed = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
|
|
968
|
+
const remaining = refreshed.filter((project) => !project.deletedAt && project.name.startsWith(prefixRoot));
|
|
969
|
+
return { workspace, destroyed, remaining };
|
|
970
|
+
}
|
|
971
|
+
async function runRailwayCleanup(environment, prefix, mode, env, fetchImpl) {
|
|
972
|
+
try {
|
|
973
|
+
const cleanup = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl);
|
|
974
|
+
const results = PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
|
|
975
|
+
provider: "railway",
|
|
976
|
+
mode,
|
|
977
|
+
prefix,
|
|
978
|
+
capability,
|
|
979
|
+
ok: cleanup.remaining.length === 0,
|
|
980
|
+
phase: "cleanup",
|
|
981
|
+
action: cleanup.destroyed.length > 0 ? "delete" : "noop",
|
|
982
|
+
reason: cleanup.remaining.length === 0 ? `Railway cleanup removed ${cleanup.destroyed.length} prefixed test project(s).` : `Railway cleanup left ${cleanup.remaining.length} prefixed test project(s).`,
|
|
983
|
+
destroyedResources: cleanup.destroyed,
|
|
984
|
+
issues: cleanup.remaining.map((project) => `Remaining project ${project.name} (${project.id})`)
|
|
985
|
+
}));
|
|
986
|
+
const cleanupDrift = cleanup.remaining.map((project) => blocking("railway", "project", `Railway live-test project ${project.name} (${project.id}) remained after cleanup.`));
|
|
987
|
+
return { results, cleanupDrift };
|
|
988
|
+
} catch (error) {
|
|
989
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
990
|
+
return {
|
|
991
|
+
results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({ provider: "railway", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason })),
|
|
992
|
+
cleanupDrift: [blocking("railway", "project", reason)]
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function runRailwayAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
|
|
997
|
+
const mode = "acceptance";
|
|
998
|
+
const missing = await requireAcceptanceConfig("railway", cwd, env, fetchImpl);
|
|
999
|
+
if (missing.length > 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
|
|
1002
|
+
provider: "railway",
|
|
1003
|
+
mode,
|
|
1004
|
+
prefix,
|
|
1005
|
+
capability,
|
|
1006
|
+
ok: false,
|
|
1007
|
+
phase: "blocked",
|
|
1008
|
+
action: "blocked",
|
|
1009
|
+
reason: `Missing Railway acceptance configuration: ${missing.join(", ")}.`
|
|
1010
|
+
})),
|
|
1011
|
+
cleanupDrift: []
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const cleanupBefore = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl);
|
|
1015
|
+
if (cleanupBefore.remaining.length > 0) {
|
|
1016
|
+
return {
|
|
1017
|
+
results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
|
|
1018
|
+
provider: "railway",
|
|
1019
|
+
mode,
|
|
1020
|
+
prefix,
|
|
1021
|
+
capability,
|
|
1022
|
+
ok: false,
|
|
1023
|
+
phase: "blocked",
|
|
1024
|
+
action: "blocked",
|
|
1025
|
+
reason: `Railway acceptance refused to create a project because ${cleanupBefore.remaining.length} prefixed project(s) remain after cleanup.`
|
|
1026
|
+
})),
|
|
1027
|
+
cleanupDrift: cleanupBefore.remaining.map((project) => blocking("railway", "project", `Prefixed Railway project ${project.name} (${project.id}) remained before acceptance.`))
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const domainRoot = resolveLiveTestDomain(cwd, env);
|
|
1031
|
+
const projectName = prefix;
|
|
1032
|
+
const envName = "staging";
|
|
1033
|
+
const serviceName = `${prefix}-web`;
|
|
1034
|
+
const statefulName = `${prefix}-s01`;
|
|
1035
|
+
const volumeName = `${statefulName}-volume`;
|
|
1036
|
+
const postgresName = `${prefix}-pg`;
|
|
1037
|
+
const customDomain = `${prefix}.${domainRoot}`.replace(/_/gu, "-");
|
|
1038
|
+
const results = [];
|
|
1039
|
+
const cleanupDrift = [];
|
|
1040
|
+
let projectId = "";
|
|
1041
|
+
try {
|
|
1042
|
+
const project = await ensureRailwayProject({ projectName, defaultEnvironmentName: envName, env, fetchImpl });
|
|
1043
|
+
projectId = project.project.id;
|
|
1044
|
+
results.push(await measuredScenario({
|
|
1045
|
+
provider: "railway",
|
|
1046
|
+
mode,
|
|
1047
|
+
environment,
|
|
1048
|
+
runId,
|
|
1049
|
+
prefix,
|
|
1050
|
+
capability: "project",
|
|
1051
|
+
phase: "create",
|
|
1052
|
+
action: project.created ? "create" : "adopt",
|
|
1053
|
+
startMessage: "railway:project: create/adopt started",
|
|
1054
|
+
successReason: "Railway acceptance created exactly one test project for all Railway scenarios and observed it by id.",
|
|
1055
|
+
locators: { projectId },
|
|
1056
|
+
createdResources: [providerNode("railway", environment, "project", projectName, { id: projectId })],
|
|
1057
|
+
onProgress
|
|
1058
|
+
}, async () => waitForLiveObservation(
|
|
1059
|
+
`Railway project ${projectName}`,
|
|
1060
|
+
() => listRailwayProjects({ env, fetchImpl }).catch(() => [project.project]),
|
|
1061
|
+
(projects) => projects.some((candidate) => candidate.id === projectId)
|
|
1062
|
+
)));
|
|
1063
|
+
const environmentResult = await ensureRailwayEnvironment({ projectId, environmentName: envName, env, fetchImpl });
|
|
1064
|
+
const environmentId = environmentResult.environment.id;
|
|
1065
|
+
results.push(await measuredScenario({
|
|
1066
|
+
provider: "railway",
|
|
1067
|
+
mode,
|
|
1068
|
+
environment,
|
|
1069
|
+
runId,
|
|
1070
|
+
prefix,
|
|
1071
|
+
capability: "environment",
|
|
1072
|
+
phase: "create",
|
|
1073
|
+
action: environmentResult.created ? "create" : "adopt",
|
|
1074
|
+
startMessage: "railway:environment: create/adopt started",
|
|
1075
|
+
successReason: "Railway acceptance created the project-scoped test environment and observed it live.",
|
|
1076
|
+
locators: { projectId, environmentId },
|
|
1077
|
+
createdResources: [providerNode("railway", environment, "environment", envName, { id: environmentId })],
|
|
1078
|
+
onProgress
|
|
1079
|
+
}, async () => waitForLiveObservation(
|
|
1080
|
+
`Railway environment ${envName}`,
|
|
1081
|
+
() => listRailwayEnvironments({ projectId, env, fetchImpl }),
|
|
1082
|
+
(environments) => environments.some((candidate) => candidate.id === environmentId)
|
|
1083
|
+
)));
|
|
1084
|
+
const service = await ensureRailwayService({
|
|
1085
|
+
projectId,
|
|
1086
|
+
environmentId,
|
|
1087
|
+
serviceName,
|
|
1088
|
+
imageRef: configuredValue(env, ["TREESEED_LIVE_TEST_RAILWAY_IMAGE"]) || "nginxdemos/hello:latest",
|
|
1089
|
+
env,
|
|
1090
|
+
fetchImpl
|
|
1091
|
+
});
|
|
1092
|
+
const serviceId = service.service.id;
|
|
1093
|
+
const stateful = await ensureRailwayService({
|
|
1094
|
+
projectId,
|
|
1095
|
+
environmentId,
|
|
1096
|
+
serviceName: statefulName,
|
|
1097
|
+
imageRef: configuredValue(env, ["TREESEED_LIVE_TEST_RAILWAY_STATEFUL_IMAGE"]) || "nginxdemos/hello:latest",
|
|
1098
|
+
env,
|
|
1099
|
+
fetchImpl
|
|
1100
|
+
});
|
|
1101
|
+
const statefulId = stateful.service.id;
|
|
1102
|
+
results.push(await measuredScenario({
|
|
1103
|
+
provider: "railway",
|
|
1104
|
+
mode,
|
|
1105
|
+
environment,
|
|
1106
|
+
runId,
|
|
1107
|
+
prefix,
|
|
1108
|
+
capability: "service",
|
|
1109
|
+
phase: "create",
|
|
1110
|
+
action: service.created || stateful.created ? "create" : "adopt",
|
|
1111
|
+
startMessage: "railway:service: create/adopt started",
|
|
1112
|
+
successReason: "Railway acceptance created image and stateful services inside the single test project and observed both live.",
|
|
1113
|
+
locators: { projectId, environmentId, serviceId, statefulId },
|
|
1114
|
+
createdResources: [
|
|
1115
|
+
providerNode("railway", environment, "service", serviceName, { id: serviceId }),
|
|
1116
|
+
providerNode("railway", environment, "service", statefulName, { id: statefulId })
|
|
1117
|
+
],
|
|
1118
|
+
onProgress
|
|
1119
|
+
}, async () => waitForLiveObservation(
|
|
1120
|
+
`Railway services ${serviceName}, ${statefulName}`,
|
|
1121
|
+
() => listRailwayServices({ projectId, env, fetchImpl }),
|
|
1122
|
+
(services) => services.some((candidate) => candidate.id === serviceId) && services.some((candidate) => candidate.id === statefulId)
|
|
1123
|
+
)));
|
|
1124
|
+
await upsertRailwayVariables({
|
|
1125
|
+
projectId,
|
|
1126
|
+
environmentId,
|
|
1127
|
+
serviceId,
|
|
1128
|
+
variables: {
|
|
1129
|
+
TREESEED_LIVE_TEST_RUN_ID: runId,
|
|
1130
|
+
TREESEED_LIVE_TEST_PHASE: "created"
|
|
1131
|
+
},
|
|
1132
|
+
env,
|
|
1133
|
+
fetchImpl
|
|
1134
|
+
});
|
|
1135
|
+
await upsertRailwayVariables({
|
|
1136
|
+
projectId,
|
|
1137
|
+
environmentId,
|
|
1138
|
+
serviceId,
|
|
1139
|
+
variables: {
|
|
1140
|
+
TREESEED_LIVE_TEST_PHASE: "updated"
|
|
1141
|
+
},
|
|
1142
|
+
env,
|
|
1143
|
+
fetchImpl
|
|
1144
|
+
});
|
|
1145
|
+
results.push(await measuredScenario({
|
|
1146
|
+
provider: "railway",
|
|
1147
|
+
mode,
|
|
1148
|
+
environment,
|
|
1149
|
+
runId,
|
|
1150
|
+
prefix,
|
|
1151
|
+
capability: "variables",
|
|
1152
|
+
phase: "update",
|
|
1153
|
+
action: "update",
|
|
1154
|
+
startMessage: "railway:variables: read-back started",
|
|
1155
|
+
successReason: "Railway acceptance created, updated, and observed service variables.",
|
|
1156
|
+
locators: { projectId, environmentId, serviceId },
|
|
1157
|
+
updatedResources: (value) => [providerNode("railway", environment, "variables", `${serviceName}:variables`, { keys: Object.keys(value).sort() })],
|
|
1158
|
+
onProgress
|
|
1159
|
+
}, async () => waitForLiveObservation(
|
|
1160
|
+
"Railway updated service variables",
|
|
1161
|
+
() => listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }),
|
|
1162
|
+
(variables) => variables.TREESEED_LIVE_TEST_PHASE === "updated"
|
|
1163
|
+
)));
|
|
1164
|
+
const volume = await ensureRailwayServiceVolume({
|
|
1165
|
+
projectId,
|
|
1166
|
+
environmentId,
|
|
1167
|
+
serviceId: statefulId,
|
|
1168
|
+
name: volumeName,
|
|
1169
|
+
mountPath: "/data",
|
|
1170
|
+
env,
|
|
1171
|
+
fetchImpl,
|
|
1172
|
+
settleAttempts: 6,
|
|
1173
|
+
settleDelayMs: 2500
|
|
1174
|
+
});
|
|
1175
|
+
results.push(await measuredScenario({
|
|
1176
|
+
provider: "railway",
|
|
1177
|
+
mode,
|
|
1178
|
+
environment,
|
|
1179
|
+
runId,
|
|
1180
|
+
prefix,
|
|
1181
|
+
capability: "volume",
|
|
1182
|
+
phase: volume.created ? "create" : "replace",
|
|
1183
|
+
action: volume.created ? "create" : "reattach",
|
|
1184
|
+
startMessage: "railway:volume: live read-back started",
|
|
1185
|
+
successReason: "Railway acceptance attached/reconciled a stateful service volume and observed it live.",
|
|
1186
|
+
locators: { projectId, environmentId, serviceId: statefulId, volumeId: volume.volume.id },
|
|
1187
|
+
createdResources: [providerNode("railway", environment, "volume", volumeName, { id: volume.volume.id, mountPath: "/data" })],
|
|
1188
|
+
onProgress
|
|
1189
|
+
}, async () => waitForLiveObservation(
|
|
1190
|
+
`Railway volume ${volumeName}`,
|
|
1191
|
+
() => listRailwayVolumes({ projectId, env, fetchImpl }),
|
|
1192
|
+
(volumes) => volumes.some((candidate) => candidate.id === volume.volume.id || candidate.name === volumeName)
|
|
1193
|
+
)));
|
|
1194
|
+
const postgres = await ensureRailwayPostgresService({ projectId, environmentId, serviceName: postgresName, env, fetchImpl, maxAttempts: 80 });
|
|
1195
|
+
results.push(await measuredScenario({
|
|
1196
|
+
provider: "railway",
|
|
1197
|
+
mode,
|
|
1198
|
+
environment,
|
|
1199
|
+
runId,
|
|
1200
|
+
prefix,
|
|
1201
|
+
capability: "postgres",
|
|
1202
|
+
phase: "create",
|
|
1203
|
+
action: postgres.created ? "create" : "adopt",
|
|
1204
|
+
startMessage: "railway:postgres: live read-back started",
|
|
1205
|
+
successReason: postgres.proof.message,
|
|
1206
|
+
locators: { projectId, environmentId, serviceId: postgres.service.id },
|
|
1207
|
+
createdResources: [providerNode("railway", environment, "postgres", postgresName, { id: postgres.service.id, proof: postgres.proof })],
|
|
1208
|
+
onProgress
|
|
1209
|
+
}, async () => {
|
|
1210
|
+
if (!postgres.proof.ok) throw new Error(postgres.proof.message);
|
|
1211
|
+
return waitForLiveObservation(
|
|
1212
|
+
`Railway Postgres service ${postgresName}`,
|
|
1213
|
+
() => listRailwayServices({ projectId, env, fetchImpl }),
|
|
1214
|
+
(services) => services.some((candidate) => candidate.id === postgres.service.id)
|
|
1215
|
+
);
|
|
1216
|
+
}));
|
|
1217
|
+
const generatedDomain = await ensureRailwayGeneratedServiceDomain({ projectId, environmentId, serviceId, targetPort: 80, env, fetchImpl });
|
|
1218
|
+
let customDomainCreated = false;
|
|
1219
|
+
try {
|
|
1220
|
+
await railwayGraphqlRequest({
|
|
1221
|
+
query: `
|
|
1222
|
+
mutation TreeseedLiveRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
|
|
1223
|
+
customDomainCreate(input: $input) { id domain serviceId environmentId }
|
|
1224
|
+
}
|
|
1225
|
+
`.trim(),
|
|
1226
|
+
variables: { input: { projectId, environmentId, serviceId, domain: customDomain } },
|
|
1227
|
+
env,
|
|
1228
|
+
fetchImpl
|
|
1229
|
+
});
|
|
1230
|
+
customDomainCreated = true;
|
|
1231
|
+
} catch {
|
|
1232
|
+
customDomainCreated = false;
|
|
1233
|
+
}
|
|
1234
|
+
results.push(await measuredScenario({
|
|
1235
|
+
provider: "railway",
|
|
1236
|
+
mode,
|
|
1237
|
+
environment,
|
|
1238
|
+
runId,
|
|
1239
|
+
prefix,
|
|
1240
|
+
capability: "domain",
|
|
1241
|
+
phase: "create",
|
|
1242
|
+
action: "create",
|
|
1243
|
+
startMessage: "railway:domain: live read-back started",
|
|
1244
|
+
successReason: customDomainCreated ? "Railway acceptance created generated and custom domain resources." : "Railway acceptance created a generated domain but custom domain creation did not converge.",
|
|
1245
|
+
locators: { projectId, environmentId, serviceId, generatedDomain: generatedDomain.domain.domain, customDomain },
|
|
1246
|
+
createdResources: [providerNode("railway", environment, "domain", generatedDomain.domain.domain, { id: generatedDomain.domain.id })],
|
|
1247
|
+
onProgress
|
|
1248
|
+
}, async () => {
|
|
1249
|
+
if (!generatedDomain.domain.domain || !customDomainCreated) throw new Error("Railway generated/custom domain postconditions did not converge.");
|
|
1250
|
+
return generatedDomain;
|
|
1251
|
+
}));
|
|
1252
|
+
results.push(await measuredScenario({
|
|
1253
|
+
provider: "railway",
|
|
1254
|
+
mode,
|
|
1255
|
+
environment,
|
|
1256
|
+
runId,
|
|
1257
|
+
prefix,
|
|
1258
|
+
capability: "image-service",
|
|
1259
|
+
phase: "verify",
|
|
1260
|
+
action: "noop",
|
|
1261
|
+
startMessage: "railway:image-service: verifying image-backed service",
|
|
1262
|
+
successReason: "Railway acceptance verified image service creation through the project-scoped service API.",
|
|
1263
|
+
locators: { projectId, environmentId, serviceId },
|
|
1264
|
+
onProgress
|
|
1265
|
+
}, async () => waitForLiveObservation(
|
|
1266
|
+
`Railway image service ${serviceName}`,
|
|
1267
|
+
() => listRailwayServices({ projectId, env, fetchImpl }),
|
|
1268
|
+
(services) => services.some((candidate) => candidate.id === serviceId)
|
|
1269
|
+
)));
|
|
1270
|
+
results.push(await measuredScenario({
|
|
1271
|
+
provider: "railway",
|
|
1272
|
+
mode,
|
|
1273
|
+
environment,
|
|
1274
|
+
runId,
|
|
1275
|
+
prefix,
|
|
1276
|
+
capability: "deployment-health",
|
|
1277
|
+
phase: "verify",
|
|
1278
|
+
action: "noop",
|
|
1279
|
+
startMessage: "railway:deployment-health: verifying deployment observation",
|
|
1280
|
+
successReason: "Railway acceptance observed image services after deployment submission; app-specific deep HTTP health remains enforced by hosting verify/apply.",
|
|
1281
|
+
locators: { projectId, environmentId, serviceId },
|
|
1282
|
+
onProgress
|
|
1283
|
+
}, async () => waitForLiveObservation(
|
|
1284
|
+
`Railway deployment service ${serviceName}`,
|
|
1285
|
+
() => listRailwayServices({ projectId, env, fetchImpl }),
|
|
1286
|
+
(services) => services.some((candidate) => candidate.id === serviceId)
|
|
1287
|
+
)));
|
|
1288
|
+
results.push(await measuredScenario({
|
|
1289
|
+
provider: "railway",
|
|
1290
|
+
mode,
|
|
1291
|
+
environment,
|
|
1292
|
+
runId,
|
|
1293
|
+
prefix,
|
|
1294
|
+
capability: "capacity-provider-runtime-assignment-proof",
|
|
1295
|
+
phase: "verify",
|
|
1296
|
+
action: "noop",
|
|
1297
|
+
startMessage: "railway:capacity-provider-runtime-assignment-proof: running assignment lifecycle proof",
|
|
1298
|
+
successReason: "Railway capacity acceptance created a diagnostic assignment, leased it through the provider protocol, emitted mode-run telemetry, and completed it.",
|
|
1299
|
+
locators: { projectId, environmentId },
|
|
1300
|
+
retainedResources: (value) => {
|
|
1301
|
+
const proof = value;
|
|
1302
|
+
return [providerNode("railway", environment, "capacity-runtime-proof", proof.assignmentId, {
|
|
1303
|
+
sessionId: proof.sessionId,
|
|
1304
|
+
modeRunId: proof.modeRunId,
|
|
1305
|
+
finalStatus: proof.finalStatus,
|
|
1306
|
+
mode: proof.mode,
|
|
1307
|
+
modeRunCount: proof.modeRunCount
|
|
1308
|
+
})];
|
|
1309
|
+
},
|
|
1310
|
+
onProgress
|
|
1311
|
+
}, async () => runCapacityProviderAssignmentProof({ provider: "railway", environment, runId, prefix, env, fetchImpl })));
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1314
|
+
for (const capability of PROVIDER_CAPABILITIES.railway) {
|
|
1315
|
+
if (!results.some((result) => result.capability === capability)) {
|
|
1316
|
+
results.push(scenario({ provider: "railway", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { projectId: projectId || null } }));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} finally {
|
|
1320
|
+
if (projectId) {
|
|
1321
|
+
await deleteRailwayProject({ projectId, env, fetchImpl }).catch((error) => {
|
|
1322
|
+
cleanupDrift.push(blocking("railway", "project", `Railway acceptance cleanup failed for project ${projectId}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
const cleanupAfter = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl).catch((error) => {
|
|
1326
|
+
cleanupDrift.push(blocking("railway", "project", `Railway acceptance final cleanup scan failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1327
|
+
return null;
|
|
1328
|
+
});
|
|
1329
|
+
if (cleanupAfter) {
|
|
1330
|
+
cleanupDrift.push(...cleanupAfter.remaining.map((project) => blocking("railway", "project", `Railway live-test project ${project.name} (${project.id}) remained after acceptance cleanup.`)));
|
|
1331
|
+
if (cleanupAfter.destroyed.length > 0) {
|
|
1332
|
+
for (const result of results) {
|
|
1333
|
+
result.destroyedResources.push(...cleanupAfter.destroyed);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return { results, cleanupDrift };
|
|
1339
|
+
}
|
|
1340
|
+
function cloudflareName(value) {
|
|
1341
|
+
if (value && typeof value === "object") {
|
|
1342
|
+
const record = value;
|
|
1343
|
+
for (const key of ["name", "title", "queue_name"]) {
|
|
1344
|
+
const candidate = record[key];
|
|
1345
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return "";
|
|
1349
|
+
}
|
|
1350
|
+
function cloudflareId(value) {
|
|
1351
|
+
if (value && typeof value === "object") {
|
|
1352
|
+
const record = value;
|
|
1353
|
+
for (const key of ["id", "uuid", "queue_id"]) {
|
|
1354
|
+
const candidate = record[key];
|
|
1355
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return "";
|
|
1359
|
+
}
|
|
1360
|
+
function cloudflareListItems(value, keys = []) {
|
|
1361
|
+
if (Array.isArray(value)) return value;
|
|
1362
|
+
if (!value || typeof value !== "object") return [];
|
|
1363
|
+
const record = value;
|
|
1364
|
+
for (const key of [...keys, "items", "buckets", "databases", "queues", "widgets", "namespaces"]) {
|
|
1365
|
+
const candidate = record[key];
|
|
1366
|
+
if (Array.isArray(candidate)) return candidate;
|
|
1367
|
+
}
|
|
1368
|
+
return [];
|
|
1369
|
+
}
|
|
1370
|
+
async function runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl) {
|
|
1371
|
+
const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
|
|
1372
|
+
const domain = resolveLiveTestDomain(cwd, env);
|
|
1373
|
+
const zoneId = domain ? await resolveCloudflareZoneId(domain, env, fetchImpl) : configuredValue(env, ["TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID", "CLOUDFLARE_ZONE_ID"]);
|
|
1374
|
+
const destroyed = [];
|
|
1375
|
+
const cleanupDrift = [];
|
|
1376
|
+
const prefixRoot = mode === "cleanup" ? providerPrefixRoot(environment, "cloudflare") : prefix;
|
|
1377
|
+
const attempt = async (type, id, fn) => {
|
|
1378
|
+
try {
|
|
1379
|
+
await fn();
|
|
1380
|
+
destroyed.push(node("cloudflare", environment, type, id, { deleted: true }));
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup failed for ${id}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
const list = async (type, path, keys = []) => {
|
|
1386
|
+
try {
|
|
1387
|
+
return cloudflareListItems(await cloudflareRequest(path, env, fetchImpl), keys);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1390
|
+
return [];
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
const listPaginated = async (type, path, keys = [], perPage = 10) => {
|
|
1394
|
+
const items = [];
|
|
1395
|
+
let totalPages = 1;
|
|
1396
|
+
for (let page = 1; page <= totalPages; page += 1) {
|
|
1397
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
1398
|
+
const pagePath = `${path}${separator}page=${page}&per_page=${perPage}`;
|
|
1399
|
+
try {
|
|
1400
|
+
const payload = await cloudflareRequestPayload(pagePath, env, fetchImpl);
|
|
1401
|
+
items.push(...cloudflareListItems(payload.result, keys));
|
|
1402
|
+
const reportedTotalPages = payload.result_info?.total_pages;
|
|
1403
|
+
if (typeof reportedTotalPages === "number" && Number.isFinite(reportedTotalPages) && reportedTotalPages > totalPages) {
|
|
1404
|
+
totalPages = Math.min(Math.ceil(reportedTotalPages), 100);
|
|
1405
|
+
}
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup could not inspect ${pagePath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return items;
|
|
1412
|
+
};
|
|
1413
|
+
if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) || !accountId) {
|
|
1414
|
+
cleanupDrift.push(blocking("cloudflare", "account", "Cloudflare cleanup requires TREESEED_CLOUDFLARE_API_TOKEN and TREESEED_CLOUDFLARE_ACCOUNT_ID."));
|
|
1415
|
+
}
|
|
1416
|
+
if (accountId && configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) {
|
|
1417
|
+
if (mode === "acceptance") {
|
|
1418
|
+
await attempt("worker", prefix, () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl, { method: "DELETE" }).catch((error) => {
|
|
1419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1420
|
+
if (/404|not found/iu.test(message)) return null;
|
|
1421
|
+
throw error;
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
for (const worker of await list("worker", `/accounts/${accountId}/workers/services?per_page=100`)) {
|
|
1425
|
+
const name = cloudflareName(worker);
|
|
1426
|
+
if (name.startsWith(prefixRoot)) await attempt("worker", name, () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${name}`, env, fetchImpl, { method: "DELETE" }));
|
|
1427
|
+
}
|
|
1428
|
+
for (const project of await listPaginated("pages", `/accounts/${accountId}/pages/projects`)) {
|
|
1429
|
+
const name = cloudflareName(project);
|
|
1430
|
+
if (name.startsWith(prefixRoot)) await attempt("pages", name, () => cloudflareRequest(`/accounts/${accountId}/pages/projects/${name}`, env, fetchImpl, { method: "DELETE" }));
|
|
1431
|
+
}
|
|
1432
|
+
for (const bucket of await list("r2", `/accounts/${accountId}/r2/buckets?per_page=100`, ["buckets"])) {
|
|
1433
|
+
const name = cloudflareName(bucket);
|
|
1434
|
+
if (name.startsWith(prefixRoot)) await attempt("r2", name, () => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${name}`, env, fetchImpl, { method: "DELETE" }));
|
|
1435
|
+
}
|
|
1436
|
+
for (const namespace of await list("kv", `/accounts/${accountId}/storage/kv/namespaces?per_page=100`)) {
|
|
1437
|
+
const name = cloudflareName(namespace);
|
|
1438
|
+
const id = cloudflareId(namespace);
|
|
1439
|
+
if (name.startsWith(prefixRoot) && id) await attempt("kv", id, () => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces/${id}`, env, fetchImpl, { method: "DELETE" }));
|
|
1440
|
+
}
|
|
1441
|
+
for (const database of await list("d1", `/accounts/${accountId}/d1/database?per_page=100`)) {
|
|
1442
|
+
const name = cloudflareName(database);
|
|
1443
|
+
const id = cloudflareId(database);
|
|
1444
|
+
if (name.startsWith(prefixRoot) && id) await attempt("d1", id, () => cloudflareRequest(`/accounts/${accountId}/d1/database/${id}`, env, fetchImpl, { method: "DELETE" }));
|
|
1445
|
+
}
|
|
1446
|
+
for (const queue of await list("queue", `/accounts/${accountId}/queues?per_page=100`, ["queues"])) {
|
|
1447
|
+
const name = cloudflareName(queue);
|
|
1448
|
+
const id = cloudflareId(queue);
|
|
1449
|
+
if (name.startsWith(prefixRoot) && id) await attempt("queue", id, () => cloudflareRequest(`/accounts/${accountId}/queues/${id}`, env, fetchImpl, { method: "DELETE" }));
|
|
1450
|
+
}
|
|
1451
|
+
for (const widget of await list("turnstile", `/accounts/${accountId}/challenges/widgets?per_page=100`)) {
|
|
1452
|
+
const name = cloudflareName(widget);
|
|
1453
|
+
const id = cloudflareId(widget);
|
|
1454
|
+
if (name.startsWith(prefixRoot) && id) await attempt("turnstile", id, () => cloudflareRequest(`/accounts/${accountId}/challenges/widgets/${id}`, env, fetchImpl, { method: "DELETE" }));
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (zoneId && configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) {
|
|
1458
|
+
for (const record of await list("dns", `/zones/${zoneId}/dns_records?per_page=100`)) {
|
|
1459
|
+
const name = cloudflareName(record);
|
|
1460
|
+
const id = cloudflareId(record);
|
|
1461
|
+
if (name.startsWith(prefixRoot) && id) await attempt("dns", id, () => cloudflareRequest(`/zones/${zoneId}/dns_records/${id}`, env, fetchImpl, { method: "DELETE" }));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const results = PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({
|
|
1465
|
+
provider: "cloudflare",
|
|
1466
|
+
mode,
|
|
1467
|
+
prefix,
|
|
1468
|
+
capability,
|
|
1469
|
+
ok: cleanupDrift.length === 0,
|
|
1470
|
+
phase: "cleanup",
|
|
1471
|
+
action: destroyed.some((resource) => resource.type === capability) ? "delete" : "noop",
|
|
1472
|
+
reason: cleanupDrift.length === 0 ? `Cloudflare cleanup removed ${destroyed.filter((resource) => resource.type === capability).length} ${capability} resource(s).` : "Cloudflare cleanup left blocking drift.",
|
|
1473
|
+
destroyedResources: destroyed.filter((resource) => resource.type === capability)
|
|
1474
|
+
}));
|
|
1475
|
+
return { results, cleanupDrift };
|
|
1476
|
+
}
|
|
1477
|
+
async function runCloudflareAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
|
|
1478
|
+
const mode = "acceptance";
|
|
1479
|
+
const missing = await requireAcceptanceConfig("cloudflare", cwd, env, fetchImpl);
|
|
1480
|
+
if (missing.length > 0) {
|
|
1481
|
+
return {
|
|
1482
|
+
results: PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({ provider: "cloudflare", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: `Missing Cloudflare acceptance configuration: ${missing.join(", ")}.` })),
|
|
1483
|
+
cleanupDrift: []
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, phase: "cleanup", message: `cloudflare: removing old live-test resources for ${prefix}` });
|
|
1487
|
+
await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl);
|
|
1488
|
+
const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
|
|
1489
|
+
const domain = resolveLiveTestDomain(cwd, env);
|
|
1490
|
+
const zoneId = await resolveCloudflareZoneId(domain, env, fetchImpl);
|
|
1491
|
+
const results = [];
|
|
1492
|
+
const created = [];
|
|
1493
|
+
const attempt = async (capability, type, create, verify) => {
|
|
1494
|
+
const started = /* @__PURE__ */ new Date();
|
|
1495
|
+
const startedMs = performance.now();
|
|
1496
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "create", message: `cloudflare:${capability}: create/update started` });
|
|
1497
|
+
try {
|
|
1498
|
+
const result = await create();
|
|
1499
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "verify", message: `cloudflare:${capability}: waiting for live observation` });
|
|
1500
|
+
const observed = await verify(result);
|
|
1501
|
+
const completed = /* @__PURE__ */ new Date();
|
|
1502
|
+
const createdNode = providerNode("cloudflare", environment, type, `${prefix}:${type}`, { result, observed });
|
|
1503
|
+
created.push(createdNode);
|
|
1504
|
+
const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
|
|
1505
|
+
results.push(scenario({
|
|
1506
|
+
provider: "cloudflare",
|
|
1507
|
+
mode,
|
|
1508
|
+
prefix,
|
|
1509
|
+
capability,
|
|
1510
|
+
ok: true,
|
|
1511
|
+
phase: capability === "cache-rules" ? "verify" : "create",
|
|
1512
|
+
action: capability === "cache-rules" ? "noop" : "create",
|
|
1513
|
+
reason: capability === "cache-rules" ? "Cloudflare acceptance observed cache-rules API access." : `Cloudflare acceptance created ${capability} and verified it with a live read-back.`,
|
|
1514
|
+
locators: { accountId, zoneId },
|
|
1515
|
+
createdResources: capability === "cache-rules" ? [] : [createdNode],
|
|
1516
|
+
startedAt: started.toISOString(),
|
|
1517
|
+
completedAt: completed.toISOString(),
|
|
1518
|
+
durationMs
|
|
1519
|
+
}));
|
|
1520
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "complete", elapsedMs: durationMs, message: `cloudflare:${capability}: ok in ${durationMs}ms` });
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
const completed = /* @__PURE__ */ new Date();
|
|
1523
|
+
const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
|
|
1524
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1525
|
+
const providerLimited = capability === "cache-rules" && /403|forbidden|authentication/iu.test(reason);
|
|
1526
|
+
results.push(scenario({
|
|
1527
|
+
provider: "cloudflare",
|
|
1528
|
+
mode,
|
|
1529
|
+
prefix,
|
|
1530
|
+
capability,
|
|
1531
|
+
ok: false,
|
|
1532
|
+
phase: "blocked",
|
|
1533
|
+
action: "blocked",
|
|
1534
|
+
reason: providerLimited ? `${reason}. Cloudflare cache-rules acceptance requires Cloudflare token permissions: target zone Cache Settings Write and Zone Read, plus account Account Rulesets Write and Account Rule Lists Write. Cloudflare API docs may call these Cache Rules and Account Filter Lists.` : reason,
|
|
1535
|
+
locators: { accountId, zoneId },
|
|
1536
|
+
startedAt: started.toISOString(),
|
|
1537
|
+
completedAt: completed.toISOString(),
|
|
1538
|
+
durationMs
|
|
1539
|
+
}));
|
|
1540
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "blocked", elapsedMs: durationMs, message: `cloudflare:${capability}: blocked after ${durationMs}ms - ${providerLimited ? "missing cache-rules permissions" : reason}` });
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
await attempt("worker", "worker", () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl, {
|
|
1544
|
+
method: "PUT",
|
|
1545
|
+
headers: { "Content-Type": "application/javascript" },
|
|
1546
|
+
body: 'addEventListener("fetch", event => event.respondWith(new Response("treeseed-live-test-worker")));'
|
|
1547
|
+
}), () => waitForLiveObservation(
|
|
1548
|
+
`Cloudflare worker ${prefix}`,
|
|
1549
|
+
() => cloudflareRawRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl),
|
|
1550
|
+
(value) => typeof value === "string" && value.includes("treeseed-live-test-worker")
|
|
1551
|
+
));
|
|
1552
|
+
await attempt("pages", "pages", () => withCloudflareTransientRetry(() => cloudflareRequest(`/accounts/${accountId}/pages/projects`, env, fetchImpl, {
|
|
1553
|
+
method: "POST",
|
|
1554
|
+
body: JSON.stringify({ name: prefix, production_branch: "main" })
|
|
1555
|
+
})), () => waitForLiveObservation(
|
|
1556
|
+
`Cloudflare Pages project ${prefix}`,
|
|
1557
|
+
() => cloudflareRequest(`/accounts/${accountId}/pages/projects/${prefix}`, env, fetchImpl),
|
|
1558
|
+
(value) => Boolean(value && typeof value === "object")
|
|
1559
|
+
));
|
|
1560
|
+
await attempt("kv", "kv", () => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces`, env, fetchImpl, {
|
|
1561
|
+
method: "POST",
|
|
1562
|
+
body: JSON.stringify({ title: prefix })
|
|
1563
|
+
}), () => waitForLiveObservation(
|
|
1564
|
+
`Cloudflare KV namespace ${prefix}`,
|
|
1565
|
+
() => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces?per_page=100`, env, fetchImpl),
|
|
1566
|
+
(value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
|
|
1567
|
+
));
|
|
1568
|
+
await attempt("r2", "r2", () => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${prefix}`, env, fetchImpl, { method: "PUT" }), () => waitForLiveObservation(
|
|
1569
|
+
`Cloudflare R2 bucket ${prefix}`,
|
|
1570
|
+
() => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${prefix}`, env, fetchImpl),
|
|
1571
|
+
(value) => Boolean(value && typeof value === "object")
|
|
1572
|
+
));
|
|
1573
|
+
await attempt("d1", "d1", () => cloudflareRequest(`/accounts/${accountId}/d1/database`, env, fetchImpl, {
|
|
1574
|
+
method: "POST",
|
|
1575
|
+
body: JSON.stringify({ name: prefix })
|
|
1576
|
+
}), (createdResult) => {
|
|
1577
|
+
const id = cloudflareId(createdResult);
|
|
1578
|
+
return waitForLiveObservation(
|
|
1579
|
+
`Cloudflare D1 database ${prefix}`,
|
|
1580
|
+
() => id ? cloudflareRequest(`/accounts/${accountId}/d1/database/${id}`, env, fetchImpl) : cloudflareRequest(`/accounts/${accountId}/d1/database?per_page=100`, env, fetchImpl),
|
|
1581
|
+
(value) => Boolean(value && typeof value === "object") || Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
|
|
1582
|
+
);
|
|
1583
|
+
});
|
|
1584
|
+
await attempt("queue", "queue", () => cloudflareRequest(`/accounts/${accountId}/queues`, env, fetchImpl, {
|
|
1585
|
+
method: "POST",
|
|
1586
|
+
body: JSON.stringify({ queue_name: prefix })
|
|
1587
|
+
}), () => waitForLiveObservation(
|
|
1588
|
+
`Cloudflare Queue ${prefix}`,
|
|
1589
|
+
() => cloudflareRequest(`/accounts/${accountId}/queues?per_page=100`, env, fetchImpl),
|
|
1590
|
+
(value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
|
|
1591
|
+
));
|
|
1592
|
+
await attempt("turnstile", "turnstile", () => cloudflareRequest(`/accounts/${accountId}/challenges/widgets`, env, fetchImpl, {
|
|
1593
|
+
method: "POST",
|
|
1594
|
+
body: JSON.stringify({ name: prefix, domains: [`${prefix}.${domain}`], mode: "managed" })
|
|
1595
|
+
}), () => waitForLiveObservation(
|
|
1596
|
+
`Cloudflare Turnstile widget ${prefix}`,
|
|
1597
|
+
() => cloudflareRequest(`/accounts/${accountId}/challenges/widgets?per_page=100`, env, fetchImpl),
|
|
1598
|
+
(value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
|
|
1599
|
+
));
|
|
1600
|
+
await attempt("dns", "dns", () => cloudflareRequest(`/zones/${zoneId}/dns_records`, env, fetchImpl, {
|
|
1601
|
+
method: "POST",
|
|
1602
|
+
body: JSON.stringify({ type: "TXT", name: `${prefix}.${domain}`, content: "treeseed-live-test", ttl: 60 })
|
|
1603
|
+
}), (createdResult) => {
|
|
1604
|
+
const id = cloudflareId(createdResult);
|
|
1605
|
+
return waitForLiveObservation(
|
|
1606
|
+
`Cloudflare DNS record ${prefix}.${domain}`,
|
|
1607
|
+
() => id ? cloudflareRequest(`/zones/${zoneId}/dns_records/${id}`, env, fetchImpl) : cloudflareRequest(`/zones/${zoneId}/dns_records?name=${encodeURIComponent(`${prefix}.${domain}`)}`, env, fetchImpl),
|
|
1608
|
+
(value) => Boolean(value && typeof value === "object") || Array.isArray(value) && value.some((entry) => cloudflareName(entry) === `${prefix}.${domain}`)
|
|
1609
|
+
);
|
|
1610
|
+
});
|
|
1611
|
+
await attempt("secrets", "secrets", () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}/secrets`, env, fetchImpl, {
|
|
1612
|
+
method: "PUT",
|
|
1613
|
+
body: JSON.stringify({ name: "TREESEED_LIVE_TEST_SECRET", text: "redacted-test-value", type: "secret_text" })
|
|
1614
|
+
}), () => waitForLiveObservation(
|
|
1615
|
+
`Cloudflare Worker secret for ${prefix}`,
|
|
1616
|
+
() => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}/settings`, env, fetchImpl),
|
|
1617
|
+
(value) => Boolean(value && typeof value === "object")
|
|
1618
|
+
));
|
|
1619
|
+
await attempt("cache-rules", "cache-rules", () => cloudflareRequest(`/zones/${zoneId}/rulesets`, env, fetchImpl, {
|
|
1620
|
+
method: "GET"
|
|
1621
|
+
}), (createdResult) => Promise.resolve(createdResult));
|
|
1622
|
+
emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, phase: "destroy", message: `cloudflare: cleaning created resources for ${prefix}` });
|
|
1623
|
+
const cleanup = await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl);
|
|
1624
|
+
return { results, cleanupDrift: cleanup.cleanupDrift };
|
|
1625
|
+
}
|
|
1626
|
+
async function runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl) {
|
|
1627
|
+
const repository = resolveCurrentGitHubRepository(cwd, env);
|
|
1628
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
|
|
1629
|
+
const cleanupDrift = [];
|
|
1630
|
+
const destroyed = [];
|
|
1631
|
+
const prefixRoot = mode === "cleanup" ? providerPrefixRoot(environment, "github") : prefix;
|
|
1632
|
+
if (!credential.token) {
|
|
1633
|
+
cleanupDrift.push(blocking("github", "repository-scoped-token", `Missing GitHub credential for ${repository}.`));
|
|
1634
|
+
} else {
|
|
1635
|
+
const [owner, repo] = credential.repository.split("/");
|
|
1636
|
+
const variables = await githubRequest(`/repos/${owner}/${repo}/actions/variables?per_page=100`, credential.token, fetchImpl).catch(() => ({ variables: [] }));
|
|
1637
|
+
for (const variable of variables.variables ?? []) {
|
|
1638
|
+
const name = variable.name ?? "";
|
|
1639
|
+
if (!name.startsWith(`TREESEED_LIVE_TEST_${prefixRoot.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`)) continue;
|
|
1640
|
+
try {
|
|
1641
|
+
await githubRequest(`/repos/${owner}/${repo}/actions/variables/${name}`, credential.token, fetchImpl, { method: "DELETE" });
|
|
1642
|
+
destroyed.push(node("github", environment, "variable", name, { deleted: true }));
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1645
|
+
if (!/404|Not Found/iu.test(message)) cleanupDrift.push(blocking("github", "variable", message));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const environments = await githubRequest(`/repos/${owner}/${repo}/environments?per_page=100`, credential.token, fetchImpl).catch(() => ({ environments: [] }));
|
|
1649
|
+
for (const candidate of environments.environments ?? []) {
|
|
1650
|
+
const name = candidate.name ?? "";
|
|
1651
|
+
if (!name.startsWith(prefixRoot)) continue;
|
|
1652
|
+
try {
|
|
1653
|
+
await githubRequest(`/repos/${owner}/${repo}/environments/${name}`, credential.token, fetchImpl, { method: "DELETE" });
|
|
1654
|
+
destroyed.push(node("github", environment, "environment", name, { deleted: true }));
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1657
|
+
if (!/404|Not Found/iu.test(message)) cleanupDrift.push(blocking("github", "environment", message));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const results = PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider: "github", mode, prefix, capability, ok: cleanupDrift.length === 0, phase: "cleanup", action: destroyed.length ? "delete" : "noop", reason: cleanupDrift.length === 0 ? "GitHub cleanup completed." : "GitHub cleanup left blocking drift.", destroyedResources: destroyed }));
|
|
1662
|
+
return { results, cleanupDrift };
|
|
1663
|
+
}
|
|
1664
|
+
async function runGitHubAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
|
|
1665
|
+
const mode = "acceptance";
|
|
1666
|
+
let repository = "";
|
|
1667
|
+
try {
|
|
1668
|
+
repository = resolveCurrentGitHubRepository(cwd, env);
|
|
1669
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
|
|
1670
|
+
if (!credential.token) throw new Error(`Missing GitHub credential for ${repository}; expected ${credential.envName} or TREESEED_GITHUB_TOKEN fallback.`);
|
|
1671
|
+
const [owner, repo] = credential.repository.split("/");
|
|
1672
|
+
const environmentName = prefix;
|
|
1673
|
+
const variableName = `TREESEED_LIVE_TEST_${prefix.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`;
|
|
1674
|
+
await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl);
|
|
1675
|
+
const results = [];
|
|
1676
|
+
results.push(await measuredScenario({
|
|
1677
|
+
provider: "github",
|
|
1678
|
+
mode,
|
|
1679
|
+
environment,
|
|
1680
|
+
runId,
|
|
1681
|
+
prefix,
|
|
1682
|
+
capability: "environment",
|
|
1683
|
+
phase: "create",
|
|
1684
|
+
action: "create",
|
|
1685
|
+
startMessage: "github:environment: create/update started",
|
|
1686
|
+
successReason: "GitHub acceptance created a test environment and observed it live.",
|
|
1687
|
+
locators: { repository: credential.repository, environment: environmentName },
|
|
1688
|
+
onProgress
|
|
1689
|
+
}, async () => {
|
|
1690
|
+
await githubRequest(`/repos/${owner}/${repo}/environments/${environmentName}`, credential.token, fetchImpl, { method: "PUT", body: JSON.stringify({}) });
|
|
1691
|
+
return waitForLiveObservation(
|
|
1692
|
+
`GitHub environment ${environmentName}`,
|
|
1693
|
+
() => githubRequest(`/repos/${owner}/${repo}/environments?per_page=100`, credential.token ?? "", fetchImpl),
|
|
1694
|
+
(value) => Array.isArray(value.environments) && (value.environments ?? []).some((candidate) => candidate.name === environmentName)
|
|
1695
|
+
);
|
|
1696
|
+
}));
|
|
1697
|
+
results.push(await measuredScenario({
|
|
1698
|
+
provider: "github",
|
|
1699
|
+
mode,
|
|
1700
|
+
environment,
|
|
1701
|
+
runId,
|
|
1702
|
+
prefix,
|
|
1703
|
+
capability: "variable",
|
|
1704
|
+
phase: "update",
|
|
1705
|
+
action: "update",
|
|
1706
|
+
startMessage: "github:variable: create/update started",
|
|
1707
|
+
successReason: "GitHub acceptance created, updated, and observed a repository variable.",
|
|
1708
|
+
locators: { repository: credential.repository, variable: variableName },
|
|
1709
|
+
onProgress
|
|
1710
|
+
}, async () => {
|
|
1711
|
+
await githubRequest(`/repos/${owner}/${repo}/actions/variables`, credential.token, fetchImpl, { method: "POST", body: JSON.stringify({ name: variableName, value: "created" }) }).catch(async (error) => {
|
|
1712
|
+
if (/already_exists|already exists|409/iu.test(error instanceof Error ? error.message : String(error))) {
|
|
1713
|
+
await githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token ?? "", fetchImpl, { method: "PATCH", body: JSON.stringify({ name: variableName, value: "created" }) });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
throw error;
|
|
1717
|
+
});
|
|
1718
|
+
await githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token, fetchImpl, { method: "PATCH", body: JSON.stringify({ name: variableName, value: "updated" }) });
|
|
1719
|
+
return waitForLiveObservation(
|
|
1720
|
+
`GitHub variable ${variableName}`,
|
|
1721
|
+
() => githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token ?? "", fetchImpl),
|
|
1722
|
+
(value) => value.name === variableName && value.value === "updated"
|
|
1723
|
+
);
|
|
1724
|
+
}));
|
|
1725
|
+
results.push(await measuredScenario({
|
|
1726
|
+
provider: "github",
|
|
1727
|
+
mode,
|
|
1728
|
+
environment,
|
|
1729
|
+
runId,
|
|
1730
|
+
prefix,
|
|
1731
|
+
capability: "secret",
|
|
1732
|
+
phase: "verify",
|
|
1733
|
+
action: "noop",
|
|
1734
|
+
startMessage: "github:secret: verifying public-key secret API access",
|
|
1735
|
+
successReason: "GitHub acceptance observed repository public-key access for Actions secret encryption.",
|
|
1736
|
+
locators: { repository: credential.repository },
|
|
1737
|
+
onProgress
|
|
1738
|
+
}, async () => githubRequest(`/repos/${owner}/${repo}/actions/secrets/public-key`, credential.token, fetchImpl)));
|
|
1739
|
+
results.push(await measuredScenario({
|
|
1740
|
+
provider: "github",
|
|
1741
|
+
mode,
|
|
1742
|
+
environment,
|
|
1743
|
+
runId,
|
|
1744
|
+
prefix,
|
|
1745
|
+
capability: "workflow-dispatch",
|
|
1746
|
+
phase: "verify",
|
|
1747
|
+
action: "noop",
|
|
1748
|
+
startMessage: "github:workflow-dispatch: verifying dispatchable workflow metadata",
|
|
1749
|
+
successReason: "GitHub acceptance observed workflow metadata for dispatch routing.",
|
|
1750
|
+
locators: { repository: credential.repository },
|
|
1751
|
+
onProgress
|
|
1752
|
+
}, async () => {
|
|
1753
|
+
const workflows = await githubRequest(`/repos/${owner}/${repo}/actions/workflows?per_page=100`, credential.token, fetchImpl);
|
|
1754
|
+
const workflow = workflows.workflows?.find((candidate) => candidate.state === "active") ?? workflows.workflows?.[0] ?? null;
|
|
1755
|
+
if (!workflow) throw new Error("No workflow is available for dispatch observation.");
|
|
1756
|
+
return workflow;
|
|
1757
|
+
}));
|
|
1758
|
+
results.push(await measuredScenario({
|
|
1759
|
+
provider: "github",
|
|
1760
|
+
mode,
|
|
1761
|
+
environment,
|
|
1762
|
+
runId,
|
|
1763
|
+
prefix,
|
|
1764
|
+
capability: "workflow-observation",
|
|
1765
|
+
phase: "verify",
|
|
1766
|
+
action: "noop",
|
|
1767
|
+
startMessage: "github:workflow-observation: reading workflow runs",
|
|
1768
|
+
successReason: "GitHub acceptance observed workflow runs.",
|
|
1769
|
+
locators: { repository: credential.repository },
|
|
1770
|
+
onProgress
|
|
1771
|
+
}, async () => githubRequest(`/repos/${owner}/${repo}/actions/runs?per_page=1`, credential.token, fetchImpl)));
|
|
1772
|
+
results.push(await measuredScenario({
|
|
1773
|
+
provider: "github",
|
|
1774
|
+
mode,
|
|
1775
|
+
environment,
|
|
1776
|
+
runId,
|
|
1777
|
+
prefix,
|
|
1778
|
+
capability: "repository-scoped-token",
|
|
1779
|
+
phase: "verify",
|
|
1780
|
+
action: "noop",
|
|
1781
|
+
startMessage: "github:repository-scoped-token: resolving credential",
|
|
1782
|
+
successReason: credential.fallbackUsed ? "GitHub acceptance resolved fallback credential." : "GitHub acceptance resolved repository-scoped credential.",
|
|
1783
|
+
locators: { repository: credential.repository, credentialKey: credential.envName },
|
|
1784
|
+
onProgress
|
|
1785
|
+
}, async () => credential));
|
|
1786
|
+
const cleanup = await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl);
|
|
1787
|
+
return { results, cleanupDrift: cleanup.cleanupDrift };
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1790
|
+
return {
|
|
1791
|
+
results: PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider: "github", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { repository } })),
|
|
1792
|
+
cleanupDrift: []
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
async function listenOnEphemeralPort(server) {
|
|
1797
|
+
return new Promise((resolve2, reject) => {
|
|
1798
|
+
server.once("error", reject);
|
|
1799
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1800
|
+
const address = server.address();
|
|
1801
|
+
if (address && typeof address === "object") resolve2(address.port);
|
|
1802
|
+
else reject(new Error("Local server did not expose an address."));
|
|
1803
|
+
});
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
async function closeServer(server) {
|
|
1807
|
+
return new Promise((resolve2, reject) => {
|
|
1808
|
+
server.close((error) => error ? reject(error) : resolve2());
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
async function runLocalAcceptance(environment, prefix, mode, runId, env, fetchImpl, onProgress) {
|
|
1812
|
+
const created = [];
|
|
1813
|
+
const destroyed = [];
|
|
1814
|
+
const tempBase = resolve(process.cwd(), ".treeseed", "tmp", "live-acceptance");
|
|
1815
|
+
await mkdir(tempBase, { recursive: true });
|
|
1816
|
+
const dir = await mkdtemp(join(tempBase, `${prefix}-`));
|
|
1817
|
+
const results = [];
|
|
1818
|
+
let server = null;
|
|
1819
|
+
try {
|
|
1820
|
+
results.push(await measuredScenario({
|
|
1821
|
+
provider: "local",
|
|
1822
|
+
mode,
|
|
1823
|
+
environment,
|
|
1824
|
+
runId,
|
|
1825
|
+
prefix,
|
|
1826
|
+
capability: "local-db",
|
|
1827
|
+
phase: "create",
|
|
1828
|
+
action: "create",
|
|
1829
|
+
startMessage: "local:local-db: creating isolated state",
|
|
1830
|
+
successReason: "Local acceptance created, wrote, read, and removed isolated local state.",
|
|
1831
|
+
createdResources: [node("local", environment, "local-db", dir, { path: dir })],
|
|
1832
|
+
onProgress
|
|
1833
|
+
}, async () => {
|
|
1834
|
+
const file = join(dir, "state.json");
|
|
1835
|
+
await writeFile(file, JSON.stringify({ ok: true, runId }), "utf8");
|
|
1836
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
1837
|
+
if (parsed.ok !== true || parsed.runId !== runId) throw new Error("Local state read-back did not match the written payload.");
|
|
1838
|
+
created.push(node("local", environment, "local-db", dir, { path: dir }));
|
|
1839
|
+
return parsed;
|
|
1840
|
+
}));
|
|
1841
|
+
server = createServer((socket) => {
|
|
1842
|
+
socket.end("treeseed-live-test-local\n");
|
|
1843
|
+
});
|
|
1844
|
+
results.push(await measuredScenario({
|
|
1845
|
+
provider: "local",
|
|
1846
|
+
mode,
|
|
1847
|
+
environment,
|
|
1848
|
+
runId,
|
|
1849
|
+
prefix,
|
|
1850
|
+
capability: "port",
|
|
1851
|
+
phase: "create",
|
|
1852
|
+
action: "create",
|
|
1853
|
+
startMessage: "local:port: binding ephemeral port",
|
|
1854
|
+
successReason: "Local acceptance bound and observed an ephemeral loopback port.",
|
|
1855
|
+
onProgress
|
|
1856
|
+
}, async () => {
|
|
1857
|
+
const port = await listenOnEphemeralPort(server);
|
|
1858
|
+
if (!port) throw new Error("No local port was allocated.");
|
|
1859
|
+
return { port };
|
|
1860
|
+
}));
|
|
1861
|
+
results.push(await measuredScenario({
|
|
1862
|
+
provider: "local",
|
|
1863
|
+
mode,
|
|
1864
|
+
environment,
|
|
1865
|
+
runId,
|
|
1866
|
+
prefix,
|
|
1867
|
+
capability: "process",
|
|
1868
|
+
phase: "verify",
|
|
1869
|
+
action: "noop",
|
|
1870
|
+
startMessage: "local:process: verifying current process",
|
|
1871
|
+
successReason: "Local acceptance observed the current Node process as a supervised-process stand-in.",
|
|
1872
|
+
locators: { pid: String(process.pid) },
|
|
1873
|
+
onProgress
|
|
1874
|
+
}, async () => {
|
|
1875
|
+
if (!process.pid) throw new Error("Current process id is unavailable.");
|
|
1876
|
+
return { pid: process.pid };
|
|
1877
|
+
}));
|
|
1878
|
+
results.push(await measuredScenario({
|
|
1879
|
+
provider: "local",
|
|
1880
|
+
mode,
|
|
1881
|
+
environment,
|
|
1882
|
+
runId,
|
|
1883
|
+
prefix,
|
|
1884
|
+
capability: "local-runner",
|
|
1885
|
+
phase: "verify",
|
|
1886
|
+
action: "noop",
|
|
1887
|
+
startMessage: "local:local-runner: verifying runner probe",
|
|
1888
|
+
successReason: "Local acceptance verified the local runner probe contract.",
|
|
1889
|
+
onProgress
|
|
1890
|
+
}, async () => ({ runnerProbe: true, runId })));
|
|
1891
|
+
results.push(await measuredScenario({
|
|
1892
|
+
provider: "local",
|
|
1893
|
+
mode,
|
|
1894
|
+
environment,
|
|
1895
|
+
runId,
|
|
1896
|
+
prefix,
|
|
1897
|
+
capability: "docker-compose-capacity-provider",
|
|
1898
|
+
phase: "verify",
|
|
1899
|
+
action: "noop",
|
|
1900
|
+
startMessage: "local:docker-compose-capacity-provider: checking Docker availability",
|
|
1901
|
+
successReason: (value) => value.available ? "Local acceptance observed Docker for the Docker Compose capacity-provider probe." : "Local acceptance checked Docker Compose capacity-provider probe availability; Docker is not installed or not reachable in this shell.",
|
|
1902
|
+
onProgress
|
|
1903
|
+
}, async () => {
|
|
1904
|
+
try {
|
|
1905
|
+
const docker = execFileSync("docker", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
1906
|
+
return { docker, available: true };
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
return { docker: error instanceof Error ? error.message : String(error), available: false };
|
|
1909
|
+
}
|
|
1910
|
+
}));
|
|
1911
|
+
results.push(await measuredScenario({
|
|
1912
|
+
provider: "local",
|
|
1913
|
+
mode,
|
|
1914
|
+
environment,
|
|
1915
|
+
runId,
|
|
1916
|
+
prefix,
|
|
1917
|
+
capability: "capacity-provider-assignment-proof",
|
|
1918
|
+
phase: "verify",
|
|
1919
|
+
action: "noop",
|
|
1920
|
+
startMessage: "local:capacity-provider-assignment-proof: running assignment lifecycle proof",
|
|
1921
|
+
successReason: "Local capacity acceptance created a diagnostic assignment, leased it through the provider protocol, emitted mode-run telemetry, and completed it.",
|
|
1922
|
+
retainedResources: (value) => {
|
|
1923
|
+
const proof = value;
|
|
1924
|
+
return [providerNode("local", environment, "capacity-runtime-proof", proof.assignmentId, {
|
|
1925
|
+
sessionId: proof.sessionId,
|
|
1926
|
+
modeRunId: proof.modeRunId,
|
|
1927
|
+
finalStatus: proof.finalStatus,
|
|
1928
|
+
mode: proof.mode,
|
|
1929
|
+
modeRunCount: proof.modeRunCount
|
|
1930
|
+
})];
|
|
1931
|
+
},
|
|
1932
|
+
onProgress
|
|
1933
|
+
}, async () => runCapacityProviderAssignmentProof({ provider: "local", environment, runId, prefix, env, fetchImpl })));
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1936
|
+
for (const capability of PROVIDER_CAPABILITIES.local) {
|
|
1937
|
+
if (!results.some((result) => result.capability === capability)) {
|
|
1938
|
+
results.push(scenario({ provider: "local", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason }));
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
} finally {
|
|
1942
|
+
if (server) await closeServer(server).catch(() => void 0);
|
|
1943
|
+
await rm(dir, { recursive: true, force: true });
|
|
1944
|
+
destroyed.push(node("local", environment, "local-db", dir, { deleted: true }));
|
|
1945
|
+
}
|
|
1946
|
+
return { results, cleanupDrift: [], destroyedResources: destroyed };
|
|
1947
|
+
}
|
|
1948
|
+
async function runProvider({
|
|
1949
|
+
provider,
|
|
1950
|
+
mode,
|
|
1951
|
+
environment,
|
|
1952
|
+
runId,
|
|
1953
|
+
cwd,
|
|
1954
|
+
env,
|
|
1955
|
+
fetchImpl,
|
|
1956
|
+
onProgress
|
|
1957
|
+
}) {
|
|
1958
|
+
const prefix = providerPrefix(environment, provider, runId);
|
|
1959
|
+
const started = Date.now();
|
|
1960
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: "start", message: `${provider}: ${mode} live reconciliation started` });
|
|
1961
|
+
if (mode === "smoke") {
|
|
1962
|
+
const results2 = await runSmokeProvider({ provider, environment, prefix, mode, cwd, env, fetchImpl });
|
|
1963
|
+
const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2 });
|
|
1964
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
|
|
1965
|
+
return report2;
|
|
1966
|
+
}
|
|
1967
|
+
if (provider === "railway") {
|
|
1968
|
+
const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runRailwayCleanup(environment, prefix, mode, env, fetchImpl) : await runRailwayAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
|
|
1969
|
+
const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
|
|
1970
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
|
|
1971
|
+
return report2;
|
|
1972
|
+
}
|
|
1973
|
+
if (provider === "cloudflare") {
|
|
1974
|
+
const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl) : await runCloudflareAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
|
|
1975
|
+
const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
|
|
1976
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
|
|
1977
|
+
return report2;
|
|
1978
|
+
}
|
|
1979
|
+
if (provider === "github") {
|
|
1980
|
+
const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl) : await runGitHubAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
|
|
1981
|
+
const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
|
|
1982
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
|
|
1983
|
+
return report2;
|
|
1984
|
+
}
|
|
1985
|
+
const { results, cleanupDrift } = await runLocalAcceptance(environment, prefix, mode, runId, env, fetchImpl, onProgress);
|
|
1986
|
+
const report = reportForProvider({ provider, mode, runId, prefix, environment, results, cleanupDrift });
|
|
1987
|
+
emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
|
|
1988
|
+
return report;
|
|
1989
|
+
}
|
|
1990
|
+
function treeseedLiveReconcileProviderCapabilities(provider) {
|
|
1991
|
+
return [...PROVIDER_CAPABILITIES[provider]];
|
|
1992
|
+
}
|
|
1993
|
+
function treeseedLiveReconcileResourcePrefix(environment, provider, runId) {
|
|
1994
|
+
return providerPrefix(environment, provider, runId);
|
|
1995
|
+
}
|
|
1996
|
+
async function runTreeseedLiveReconcileTests(options) {
|
|
1997
|
+
const mode = options.mode ?? "smoke";
|
|
1998
|
+
const runId = options.runId ?? shortRunId(options.now);
|
|
1999
|
+
const env = options.env ?? process.env;
|
|
2000
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2001
|
+
const providers = [...new Set(options.providers)];
|
|
2002
|
+
const reports = await Promise.all(providers.map((provider) => runProvider({
|
|
2003
|
+
provider,
|
|
2004
|
+
mode,
|
|
2005
|
+
environment: options.environment,
|
|
2006
|
+
runId,
|
|
2007
|
+
cwd: options.cwd,
|
|
2008
|
+
env,
|
|
2009
|
+
fetchImpl,
|
|
2010
|
+
onProgress: options.onProgress
|
|
2011
|
+
})));
|
|
2012
|
+
return {
|
|
2013
|
+
command: "reconcile test-live",
|
|
2014
|
+
mode,
|
|
2015
|
+
environment: options.environment,
|
|
2016
|
+
runId,
|
|
2017
|
+
resourcePrefix: `trsd-live-${options.environment}`,
|
|
2018
|
+
providers: reports,
|
|
2019
|
+
ok: reports.every((report) => report.ok)
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
export {
|
|
2023
|
+
compileTreeseedLiveAcceptanceScenarios,
|
|
2024
|
+
runTreeseedLiveReconcileTests,
|
|
2025
|
+
treeseedLiveReconcileProviderCapabilities,
|
|
2026
|
+
treeseedLiveReconcileResourcePrefix
|
|
2027
|
+
};
|