@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
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
compileTreeseedDesiredResourceGraph,
|
|
6
|
+
compileTreeseedDesiredUnitsFromGraph
|
|
7
|
+
} from "../platform/desired-state.js";
|
|
8
|
+
import {
|
|
9
|
+
collectTreeseedReconcileStatus,
|
|
10
|
+
planTreeseedReconciliation,
|
|
11
|
+
reconcileTreeseedTarget
|
|
12
|
+
} from "../reconcile/index.js";
|
|
4
13
|
import {
|
|
5
14
|
applyTreeseedEnvironmentToProcess,
|
|
6
15
|
applyTreeseedConfigValues,
|
|
@@ -8,6 +17,7 @@ import {
|
|
|
8
17
|
assertTreeseedCommandEnvironment,
|
|
9
18
|
checkTreeseedProviderConnections,
|
|
10
19
|
collectTreeseedConfigContext,
|
|
20
|
+
collectTreeseedConfigSeedValues,
|
|
11
21
|
collectTreeseedPrintEnvReport,
|
|
12
22
|
ensureTreeseedSecretSessionForConfig,
|
|
13
23
|
ensureTreeseedActVerificationTooling,
|
|
@@ -18,28 +28,24 @@ import {
|
|
|
18
28
|
inspectTreeseedKeyAgentStatus,
|
|
19
29
|
loadTreeseedMachineConfig,
|
|
20
30
|
resolveTreeseedLaunchEnvironment,
|
|
31
|
+
resolveTreeseedMachineEnvironmentValues,
|
|
21
32
|
resolveTreeseedRemoteSession,
|
|
22
33
|
rotateTreeseedMachineKey,
|
|
23
34
|
setTreeseedRemoteSession,
|
|
24
35
|
writeTreeseedMachineConfig
|
|
25
36
|
} from "../operations/services/config-runtime.js";
|
|
26
|
-
import {
|
|
37
|
+
import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
27
38
|
import { ControlPlaneClient } from "../control-plane-client.js";
|
|
28
39
|
import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
|
|
29
40
|
import {
|
|
30
|
-
assertDeploymentInitialized,
|
|
31
41
|
buildProvisioningSummary,
|
|
32
42
|
cleanupDestroyedState,
|
|
33
43
|
createBranchPreviewDeployTarget,
|
|
34
44
|
createPersistentDeployTarget,
|
|
35
|
-
destroyCloudflareResources,
|
|
36
45
|
destroyTreeseedEnvironmentResources,
|
|
37
|
-
ensureGeneratedWranglerConfig,
|
|
38
|
-
finalizeDeploymentState,
|
|
39
46
|
loadDeployState,
|
|
40
47
|
recordHostedDeploymentState,
|
|
41
|
-
|
|
42
|
-
validateDeployPrerequisites,
|
|
48
|
+
resolveConfiguredSurfaceDomain,
|
|
43
49
|
validateDestroyPrerequisites
|
|
44
50
|
} from "../operations/services/deploy.js";
|
|
45
51
|
import {
|
|
@@ -48,7 +54,6 @@ import {
|
|
|
48
54
|
assertFeatureBranch,
|
|
49
55
|
branchExists,
|
|
50
56
|
checkoutBranch,
|
|
51
|
-
checkoutDetachedOriginBranch,
|
|
52
57
|
checkoutTaskBranchFromStaging,
|
|
53
58
|
createDeprecatedTaskTag,
|
|
54
59
|
deleteLocalBranch,
|
|
@@ -57,35 +62,36 @@ import {
|
|
|
57
62
|
gitWorkflowRoot,
|
|
58
63
|
headCommit,
|
|
59
64
|
listTaskBranches,
|
|
60
|
-
|
|
61
|
-
prepareReleaseBranches,
|
|
65
|
+
mergeBranchDownIntoFeature,
|
|
62
66
|
PRODUCTION_BRANCH,
|
|
67
|
+
promoteCommitToBranchWithExpectedHead,
|
|
63
68
|
pushBranch,
|
|
64
|
-
pushHeadToBranch,
|
|
65
69
|
reattachDetachedHeadIfSafe,
|
|
70
|
+
remoteHeadCommit,
|
|
66
71
|
remoteBranchExists,
|
|
67
72
|
STAGING_BRANCH,
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
syncBranchWithOrigin,
|
|
74
|
+
waitForStagingAutomation
|
|
70
75
|
} from "../operations/services/git-workflow.js";
|
|
71
76
|
import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
|
|
72
|
-
import {
|
|
77
|
+
import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
|
|
73
78
|
import {
|
|
74
79
|
formatGitHubActionsGateFailure,
|
|
75
80
|
inspectGitHubActionsVerification,
|
|
76
81
|
skippedGitHubActionsGate,
|
|
77
82
|
waitForGitHubActionsGate
|
|
78
83
|
} from "../operations/services/github-actions-verification.js";
|
|
79
|
-
import {
|
|
80
|
-
|
|
81
|
-
} from "../operations/services/release-
|
|
84
|
+
import { cleanProofLedger } from "../operations/services/release-proof-ledger.js";
|
|
85
|
+
import { buildTreeseedProofPlan, hostedWorkflowForPackage, summarizeTreeseedProofLedger } from "../operations/services/release-proof-planner.js";
|
|
86
|
+
import { runTreeseedProof } from "../operations/services/release-proof-runner.js";
|
|
87
|
+
import { createTreeseedWorkflowTimer } from "../operations/services/workflow-timing.js";
|
|
82
88
|
import {
|
|
83
89
|
collectReleaseHistoryCommits,
|
|
84
90
|
renderAdministrativeCommitMessage,
|
|
85
91
|
upsertReleaseChangelog
|
|
86
92
|
} from "../operations/services/release-history.js";
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
93
|
+
import { packageScriptPath } from "../operations/services/runtime-tools.js";
|
|
94
|
+
import { loadTreeseedPlatformConfig } from "../platform/config.js";
|
|
89
95
|
import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
|
|
90
96
|
import {
|
|
91
97
|
collectMergeConflictReport,
|
|
@@ -102,18 +108,14 @@ import {
|
|
|
102
108
|
} from "../operations/services/workspace-save.js";
|
|
103
109
|
import {
|
|
104
110
|
planRepositorySave,
|
|
105
|
-
refreshAndValidateRootWorkspaceLockfileForSave,
|
|
106
111
|
repositorySaveErrorDetails,
|
|
107
112
|
runRepositorySaveOrchestrator
|
|
108
113
|
} from "../operations/services/repository-save-orchestrator.js";
|
|
114
|
+
import { discoverTreeseedPackageAdapters } from "../operations/services/package-adapters.js";
|
|
109
115
|
import {
|
|
110
|
-
assertNoInternalDevReferences,
|
|
111
|
-
cleanupStaleTreeseedDevTags,
|
|
112
|
-
collectTreeseedDevTagCleanupPlan,
|
|
113
116
|
collectInternalDevReferenceIssues,
|
|
114
|
-
|
|
115
|
-
normalizeGitRemoteForManifest
|
|
116
|
-
rewriteProjectInternalDependenciesToStableVersions
|
|
117
|
+
installableInternalDependencyVersions,
|
|
118
|
+
normalizeGitRemoteForManifest
|
|
117
119
|
} from "../operations/services/package-reference-policy.js";
|
|
118
120
|
import {
|
|
119
121
|
ensureLocalWorkspaceLinks,
|
|
@@ -121,7 +123,6 @@ import {
|
|
|
121
123
|
unlinkLocalWorkspaceLinks
|
|
122
124
|
} from "../operations/services/workspace-dependency-mode.js";
|
|
123
125
|
import {
|
|
124
|
-
hasCompleteTreeseedPackageCheckout,
|
|
125
126
|
changedWorkspacePackages,
|
|
126
127
|
publishableWorkspacePackages,
|
|
127
128
|
run,
|
|
@@ -129,9 +130,13 @@ import {
|
|
|
129
130
|
workspacePackages,
|
|
130
131
|
workspaceRoot
|
|
131
132
|
} from "../operations/services/workspace-tools.js";
|
|
132
|
-
import {
|
|
133
|
+
import { classifyTreeseedGitMode, runTreeseedGit, runTreeseedGitOk, runTreeseedGitText } from "../operations/services/git-runner.js";
|
|
134
|
+
import { collectTreeseedDeploymentReadiness } from "../operations/services/deployment-readiness.js";
|
|
135
|
+
import { collectTreeseedLiveHostedServiceChecks } from "../operations/services/live-hosted-service-checks.js";
|
|
136
|
+
import { discoverTreeseedApplications } from "../hosting/apps.js";
|
|
137
|
+
import { compileTreeseedHostingGraph } from "../hosting/graph.js";
|
|
133
138
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
134
|
-
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits,
|
|
139
|
+
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, destroyTreeseedTargetUnits, filterTreeseedDesiredUnitsByBootstrapSystems, resolveTreeseedBootstrapSelection } from "../reconcile/index.js";
|
|
135
140
|
import {
|
|
136
141
|
acquireWorkflowLock,
|
|
137
142
|
archiveWorkflowRun,
|
|
@@ -153,6 +158,7 @@ import {
|
|
|
153
158
|
checkedOutWorkspacePackageRepos,
|
|
154
159
|
resolveTreeseedWorkflowSession
|
|
155
160
|
} from "./session.js";
|
|
161
|
+
import { checkedOutManagedWorkflowRepos } from "../operations/services/managed-repositories.js";
|
|
156
162
|
import {
|
|
157
163
|
classifyTreeseedBranchRole,
|
|
158
164
|
resolveTreeseedWorkflowPaths
|
|
@@ -184,6 +190,14 @@ function defaultWrite(output, stream = "stdout") {
|
|
|
184
190
|
(stream === "stderr" ? process.stderr : process.stdout).write(`${output}
|
|
185
191
|
`);
|
|
186
192
|
}
|
|
193
|
+
function runGit(args, options) {
|
|
194
|
+
return runTreeseedGitText(args, {
|
|
195
|
+
cwd: options.cwd,
|
|
196
|
+
mode: classifyTreeseedGitMode(args),
|
|
197
|
+
timeoutMs: options.timeoutMs,
|
|
198
|
+
maxBuffer: options.maxBuffer
|
|
199
|
+
});
|
|
200
|
+
}
|
|
187
201
|
function shouldManageWorkspaceLinks(mode, env = process.env) {
|
|
188
202
|
if (mode === "off") return false;
|
|
189
203
|
const envMode = String(env?.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
|
|
@@ -214,8 +228,10 @@ function readPackageScript(root, packageDir, scriptName) {
|
|
|
214
228
|
function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
|
|
215
229
|
const packages = [
|
|
216
230
|
{ name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
|
|
217
|
-
{ name: "@treeseed/
|
|
231
|
+
{ name: "@treeseed/ui", dir: "packages/ui", artifacts: ["dist/index.js"] },
|
|
232
|
+
{ name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/manager.js", "dist/provider/runner.js"] },
|
|
218
233
|
{ name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
|
|
234
|
+
{ name: "@treeseed/admin", dir: "packages/admin", artifacts: ["dist/plugin.js"] },
|
|
219
235
|
{ name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
|
|
220
236
|
];
|
|
221
237
|
for (const entry of packages) {
|
|
@@ -257,7 +273,7 @@ function ensureWorkflowCommandBins(root, helpers) {
|
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
function unresolvedMergePaths(repoDir) {
|
|
260
|
-
return
|
|
276
|
+
return runGit(["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
261
277
|
}
|
|
262
278
|
function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
263
279
|
const gitRoot = repoRoot(root);
|
|
@@ -278,7 +294,7 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
|
278
294
|
const entries = [];
|
|
279
295
|
for (const pkg of packages) {
|
|
280
296
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
281
|
-
|
|
297
|
+
runGit(["add", pkg.repoPath], { cwd: gitRoot });
|
|
282
298
|
entries.push({
|
|
283
299
|
packageName: pkg.name,
|
|
284
300
|
path: pkg.repoPath,
|
|
@@ -307,9 +323,14 @@ function normalizeCiMode(mode, operation) {
|
|
|
307
323
|
if (mode === "hosted" || mode === "off") return mode;
|
|
308
324
|
return operation === "save" ? "off" : "hosted";
|
|
309
325
|
}
|
|
310
|
-
function
|
|
326
|
+
function normalizeSaveLane(lane) {
|
|
327
|
+
const value = lane ?? process.env.TREESEED_SAVE_LANE;
|
|
328
|
+
return value === "promotion" ? "promotion" : "fast";
|
|
329
|
+
}
|
|
330
|
+
function normalizeSaveCiMode(mode, branch, lane = "fast") {
|
|
311
331
|
if (mode === "hosted" || mode === "off") return mode;
|
|
312
|
-
return branch === STAGING_BRANCH ? "hosted" : "off";
|
|
332
|
+
if (lane === "promotion") return branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH ? "hosted" : "off";
|
|
333
|
+
return "off";
|
|
313
334
|
}
|
|
314
335
|
function normalizeSaveVerifyMode(mode) {
|
|
315
336
|
switch (mode) {
|
|
@@ -329,8 +350,16 @@ function normalizeSaveVerifyMode(mode) {
|
|
|
329
350
|
return "skip";
|
|
330
351
|
}
|
|
331
352
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
353
|
+
function normalizeReleaseCandidateMode(mode, operation, lane = "fast") {
|
|
354
|
+
const value = mode ?? process.env.TREESEED_RELEASE_CANDIDATE_MODE;
|
|
355
|
+
if (value === "hybrid" || value === "strict" || value === "skip") {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
if (operation === "save" && lane === "promotion") return "strict";
|
|
359
|
+
return operation === "save" ? "hybrid" : "strict";
|
|
360
|
+
}
|
|
361
|
+
function shouldUseHostedSaveCi(input, branch, lane = normalizeSaveLane(input.lane)) {
|
|
362
|
+
return normalizeSaveCiMode(input.ciMode, branch, lane) === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
|
|
334
363
|
}
|
|
335
364
|
function worktreePayload(root, requestedMode) {
|
|
336
365
|
const metadata = managedWorkflowWorktreeMetadata(root);
|
|
@@ -354,35 +383,27 @@ function helpersForCwd(helpers, cwd) {
|
|
|
354
383
|
function shouldDispatchSwitchToManagedWorktree(root, input, env) {
|
|
355
384
|
return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
|
|
356
385
|
}
|
|
357
|
-
function
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
"github_auth_unavailable",
|
|
368
|
-
[
|
|
369
|
-
"Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
|
|
370
|
-
github.detail,
|
|
371
|
-
`Managed gh check: ${command}`,
|
|
372
|
-
"Remediation:",
|
|
373
|
-
...github.remediation.map((item) => `- ${item}`)
|
|
374
|
-
].join("\n"),
|
|
375
|
-
{
|
|
376
|
-
details: {
|
|
377
|
-
toolsHome: tools.toolsHome,
|
|
378
|
-
ghConfigDir: tools.ghConfigDir,
|
|
379
|
-
github,
|
|
380
|
-
tools: tools.tools
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
);
|
|
386
|
+
function assertHostedGitHubWorkflowCredentialsReady(operation, root, gates) {
|
|
387
|
+
const missing = [];
|
|
388
|
+
for (const gate of gates) {
|
|
389
|
+
const repository = gate.repository ?? resolveGitHubRepositorySlug(gate.repoPath);
|
|
390
|
+
const scope = gate.branch === PRODUCTION_BRANCH ? "prod" : "staging";
|
|
391
|
+
const values = resolveTreeseedMachineEnvironmentValues(root, scope);
|
|
392
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
|
|
393
|
+
if (!credential.token) {
|
|
394
|
+
missing.push({ name: gate.name, repository: credential.repository, envName: credential.envName });
|
|
395
|
+
}
|
|
384
396
|
}
|
|
385
|
-
return
|
|
397
|
+
if (missing.length === 0) return;
|
|
398
|
+
workflowError(
|
|
399
|
+
operation,
|
|
400
|
+
"github_auth_unavailable",
|
|
401
|
+
[
|
|
402
|
+
"Treeseed hosted GitHub workflow gates require Treeseed-prefixed GitHub credentials.",
|
|
403
|
+
...missing.map((gate) => `- ${gate.name}: configure ${gate.envName} for ${gate.repository}, or TREESEED_GITHUB_TOKEN as a fallback.`)
|
|
404
|
+
].join("\n"),
|
|
405
|
+
{ details: { missing } }
|
|
406
|
+
);
|
|
386
407
|
}
|
|
387
408
|
async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
388
409
|
if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
@@ -391,41 +412,49 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
391
412
|
if (gates.length === 0) {
|
|
392
413
|
return [];
|
|
393
414
|
}
|
|
394
|
-
|
|
415
|
+
if (operation === "save" && gates.every((gate) => !gate.repository && githubRepositoryForRepo(gate.repoPath) == null)) {
|
|
416
|
+
return gates.map((gate) => skippedGitHubActionsGate(gate, "non-github-repository"));
|
|
417
|
+
}
|
|
418
|
+
assertHostedGitHubWorkflowCredentialsReady(operation, options.root ?? gates[0].repoPath, gates);
|
|
395
419
|
const results = [];
|
|
396
420
|
for (const gate of gates) {
|
|
421
|
+
const gateWithTimeout = {
|
|
422
|
+
...gate,
|
|
423
|
+
timeoutSeconds: gate.timeoutSeconds ?? HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS
|
|
424
|
+
};
|
|
397
425
|
if (options.root && options.runId) {
|
|
398
426
|
const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
|
|
399
|
-
repository:
|
|
400
|
-
workflow:
|
|
401
|
-
headSha:
|
|
402
|
-
branch:
|
|
427
|
+
repository: gateWithTimeout.repository ?? null,
|
|
428
|
+
workflow: gateWithTimeout.workflow,
|
|
429
|
+
headSha: gateWithTimeout.headSha,
|
|
430
|
+
branch: gateWithTimeout.branch
|
|
403
431
|
});
|
|
404
432
|
if (cached) {
|
|
405
433
|
results.push({
|
|
406
434
|
...cached.result,
|
|
407
|
-
name:
|
|
435
|
+
name: gateWithTimeout.name,
|
|
408
436
|
cached: true
|
|
409
437
|
});
|
|
410
438
|
continue;
|
|
411
439
|
}
|
|
412
440
|
}
|
|
413
|
-
const result = await waitForGitHubActionsGate(
|
|
441
|
+
const result = await waitForGitHubActionsGate(gateWithTimeout, {
|
|
414
442
|
operation,
|
|
443
|
+
env: githubWorkflowGateEnv(options.root, gateWithTimeout),
|
|
415
444
|
onProgress: options.onProgress
|
|
416
445
|
});
|
|
417
446
|
const normalized = {
|
|
418
|
-
name:
|
|
447
|
+
name: gateWithTimeout.name,
|
|
419
448
|
...result,
|
|
420
|
-
workflow: String(result.workflow ??
|
|
421
|
-
branch: String(result.branch ??
|
|
422
|
-
headSha: String(result.headSha ??
|
|
423
|
-
timeoutSeconds:
|
|
449
|
+
workflow: String(result.workflow ?? gateWithTimeout.workflow),
|
|
450
|
+
branch: String(result.branch ?? gateWithTimeout.branch),
|
|
451
|
+
headSha: String(result.headSha ?? gateWithTimeout.headSha),
|
|
452
|
+
timeoutSeconds: gateWithTimeout.timeoutSeconds,
|
|
424
453
|
cached: false
|
|
425
454
|
};
|
|
426
455
|
if (normalized.status === "completed" && normalized.conclusion !== "success") {
|
|
427
|
-
workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(
|
|
428
|
-
details: { gate, workflow: normalized }
|
|
456
|
+
workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gateWithTimeout, normalized), {
|
|
457
|
+
details: { gate: gateWithTimeout, workflow: normalized }
|
|
429
458
|
});
|
|
430
459
|
}
|
|
431
460
|
if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
|
|
@@ -435,13 +464,146 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
435
464
|
}
|
|
436
465
|
return results;
|
|
437
466
|
}
|
|
467
|
+
function githubWorkflowGateEnv(root, gate) {
|
|
468
|
+
if (!root) return process.env;
|
|
469
|
+
try {
|
|
470
|
+
const repository = gate.repository ?? resolveGitHubRepositorySlug(gate.repoPath);
|
|
471
|
+
const scope = gate.branch === PRODUCTION_BRANCH ? "prod" : "staging";
|
|
472
|
+
const values = resolveTreeseedMachineEnvironmentValues(root, scope);
|
|
473
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
|
|
474
|
+
if (!credential.token) return process.env;
|
|
475
|
+
return {
|
|
476
|
+
...process.env,
|
|
477
|
+
TREESEED_GITHUB_TOKEN: credential.token,
|
|
478
|
+
GH_TOKEN: credential.token,
|
|
479
|
+
GITHUB_TOKEN: credential.token
|
|
480
|
+
};
|
|
481
|
+
} catch {
|
|
482
|
+
return process.env;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
438
485
|
const HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
|
|
486
|
+
const HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS = 45 * 60;
|
|
439
487
|
function hostedDeployGate(gate) {
|
|
440
488
|
return {
|
|
441
489
|
...gate,
|
|
442
490
|
timeoutSeconds: gate.timeoutSeconds ?? HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS
|
|
443
491
|
};
|
|
444
492
|
}
|
|
493
|
+
function saveHostedEnvironmentForBranch(branch) {
|
|
494
|
+
if (branch === STAGING_BRANCH) return "staging";
|
|
495
|
+
if (branch === PRODUCTION_BRANCH) return "prod";
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function selectorFromWorkflowHostingGraph(graph) {
|
|
499
|
+
const includesApi = graph.units.some((unit) => unit.id === "api" || unit.config.serviceName === "treeseed-api");
|
|
500
|
+
const scope = graph.environment;
|
|
501
|
+
const target = createPersistentDeployTarget(scope);
|
|
502
|
+
const webDomain = resolveConfiguredSurfaceDomain(graph.deployConfig, target, "web");
|
|
503
|
+
const apiDomain = resolveConfiguredSurfaceDomain(graph.deployConfig, target, "api");
|
|
504
|
+
const domainServiceIds = [
|
|
505
|
+
webDomain,
|
|
506
|
+
webDomain ? `web:${webDomain}` : null,
|
|
507
|
+
apiDomain,
|
|
508
|
+
apiDomain ? `api:${apiDomain}` : null
|
|
509
|
+
];
|
|
510
|
+
return {
|
|
511
|
+
host: [...new Set([
|
|
512
|
+
...graph.units.map((unit) => unit.host.id),
|
|
513
|
+
...includesApi ? ["cloudflare-dns"] : []
|
|
514
|
+
].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
|
|
515
|
+
serviceId: [...new Set(graph.units.flatMap((unit) => [
|
|
516
|
+
unit.id,
|
|
517
|
+
typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
|
|
518
|
+
]).concat(domainServiceIds).filter((value) => Boolean(value)))],
|
|
519
|
+
serviceType: [...new Set(graph.units.flatMap((unit) => {
|
|
520
|
+
if (unit.id === "api") return ["api-runtime", "railway-service:api", "custom-domain:api", "dns-record"];
|
|
521
|
+
if (unit.id === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
|
|
522
|
+
if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
|
|
523
|
+
if (unit.host.id === "cloudflare") return ["web-ui", "edge-worker", "content-store", "database", "kv-form-guard", "turnstile-widget", "pages-project", "custom-domain:web", "dns-record"];
|
|
524
|
+
return [];
|
|
525
|
+
}))]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId) {
|
|
529
|
+
const graph = compileTreeseedHostingGraph({ tenantRoot: root, environment });
|
|
530
|
+
const selector = selectorFromWorkflowHostingGraph(graph);
|
|
531
|
+
if (process.env.TREESEED_WORKFLOW_HOSTED_RECONCILE_MODE === "skip") {
|
|
532
|
+
return {
|
|
533
|
+
status: "skipped",
|
|
534
|
+
reason: "disabled",
|
|
535
|
+
environment,
|
|
536
|
+
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
537
|
+
selectedResources: graph.units.map((unit) => ({
|
|
538
|
+
id: unit.id,
|
|
539
|
+
host: unit.host.id,
|
|
540
|
+
serviceType: unit.serviceType.id,
|
|
541
|
+
placement: unit.placement,
|
|
542
|
+
serviceName: typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
|
|
543
|
+
}))
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const target = createPersistentDeployTarget(environment);
|
|
547
|
+
const env = {
|
|
548
|
+
...helpers.context.env,
|
|
549
|
+
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
|
|
550
|
+
};
|
|
551
|
+
helpers.write(`[save][workflow] Reconciling ${environment} hosted deployments for ${graph.units.length} selected resources.`);
|
|
552
|
+
const reconcile = await reconcileTreeseedTarget({
|
|
553
|
+
tenantRoot: root,
|
|
554
|
+
target,
|
|
555
|
+
env,
|
|
556
|
+
selector,
|
|
557
|
+
dryRun: false,
|
|
558
|
+
write: (line) => helpers.write(`[save][reconcile] ${line}`, "stderr"),
|
|
559
|
+
session: /* @__PURE__ */ new Map([["workflowRunId", workflowRunId]])
|
|
560
|
+
});
|
|
561
|
+
const status = await collectTreeseedReconcileStatus({
|
|
562
|
+
tenantRoot: root,
|
|
563
|
+
target,
|
|
564
|
+
env,
|
|
565
|
+
selector
|
|
566
|
+
});
|
|
567
|
+
if (!status.ready) {
|
|
568
|
+
workflowError("save", "hosted_reconcile_failed", `Hosted reconciliation for ${environment} did not verify:
|
|
569
|
+
${status.blockers.join("\n")}`, {
|
|
570
|
+
details: { environment, selector, status, reconcile }
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const live = await collectTreeseedLiveHostedServiceChecks({
|
|
574
|
+
tenantRoot: root,
|
|
575
|
+
target: environment,
|
|
576
|
+
strict: true,
|
|
577
|
+
requireLiveRailway: true,
|
|
578
|
+
requireLiveHttp: true,
|
|
579
|
+
env
|
|
580
|
+
});
|
|
581
|
+
const liveFailures = [
|
|
582
|
+
...live.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
|
|
583
|
+
...live.liveObservation.issues
|
|
584
|
+
];
|
|
585
|
+
if (liveFailures.length > 0) {
|
|
586
|
+
workflowError("save", "hosted_live_verification_failed", `Hosted live verification for ${environment} failed:
|
|
587
|
+
${liveFailures.join("\n")}`, {
|
|
588
|
+
details: { environment, selector, live, reconcile }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
status: "reconciled",
|
|
593
|
+
environment,
|
|
594
|
+
selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
|
|
595
|
+
selectedResources: graph.units.map((unit) => ({
|
|
596
|
+
id: unit.id,
|
|
597
|
+
host: unit.host.id,
|
|
598
|
+
serviceType: unit.serviceType.id,
|
|
599
|
+
placement: unit.placement,
|
|
600
|
+
serviceName: typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
|
|
601
|
+
})),
|
|
602
|
+
reconcile,
|
|
603
|
+
postApplyStatus: status,
|
|
604
|
+
liveVerification: live
|
|
605
|
+
};
|
|
606
|
+
}
|
|
445
607
|
function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
|
|
446
608
|
const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
|
|
447
609
|
const releaseRecord = stringRecord(rootRelease) ?? {};
|
|
@@ -507,6 +669,22 @@ ${missing.map((check) => `${check.id}: ${check.path}`).join("\n")}`, {
|
|
|
507
669
|
}
|
|
508
670
|
return report;
|
|
509
671
|
}
|
|
672
|
+
function ensureTreeseedLocalStateExcluded(root) {
|
|
673
|
+
const gitDir = runTreeseedGit(["rev-parse", "--git-dir"], { cwd: root, mode: "read", allowFailure: true }).stdout.trim();
|
|
674
|
+
if (!gitDir) return;
|
|
675
|
+
const excludePath = resolve(root, gitDir, "info", "exclude");
|
|
676
|
+
const current = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
677
|
+
const requiredEntries = ["/.treeseed/config/", "/.treeseed/workflow/", "/.treeseed/state/", "/.treeseed/workspace-links.json"];
|
|
678
|
+
const missing = requiredEntries.filter((entry) => !current.split(/\r?\n/u).includes(entry));
|
|
679
|
+
if (missing.length === 0) return;
|
|
680
|
+
mkdirSync(dirname(excludePath), { recursive: true });
|
|
681
|
+
writeFileSync(
|
|
682
|
+
excludePath,
|
|
683
|
+
`${current}${current.endsWith("\n") || current.length === 0 ? "" : "\n"}${missing.join("\n")}
|
|
684
|
+
`,
|
|
685
|
+
"utf8"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
510
688
|
function workflowError(operation, code, message, options = {}) {
|
|
511
689
|
throw new TreeseedWorkflowError(operation, code, message, options);
|
|
512
690
|
}
|
|
@@ -609,6 +787,9 @@ function createWorkspaceRootRepoReport(root) {
|
|
|
609
787
|
function createWorkspacePackageReports(root) {
|
|
610
788
|
return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
|
|
611
789
|
}
|
|
790
|
+
function createManagedWorkflowRepoReports(root) {
|
|
791
|
+
return checkedOutManagedWorkflowRepos(root).map((repo) => createRepoReport(repo.name, repo.dir, currentBranch(repo.dir) || null, hasMeaningfulChanges(repo.dir)));
|
|
792
|
+
}
|
|
612
793
|
function findReportByName(reports, name) {
|
|
613
794
|
return reports.find((report) => report.name === name) ?? null;
|
|
614
795
|
}
|
|
@@ -616,15 +797,20 @@ function findReportByPath(reports, path) {
|
|
|
616
797
|
return reports.find((report) => report.path === path) ?? null;
|
|
617
798
|
}
|
|
618
799
|
function assertWorkspaceClean(root) {
|
|
619
|
-
const repoDirs = [repoRoot(root), ...
|
|
800
|
+
const repoDirs = [repoRoot(root), ...checkedOutManagedWorkflowRepos(root).map((repo) => repo.dir)];
|
|
620
801
|
assertCleanWorktrees(repoDirs);
|
|
621
802
|
return repoDirs;
|
|
622
803
|
}
|
|
623
804
|
function buildWorkflowResult(operation, cwd, payload, options = {}) {
|
|
805
|
+
const timing = options.timing ?? createTreeseedWorkflowTimer().finish();
|
|
624
806
|
const resolvedPayload = options.includeFinalState ?? true ? {
|
|
625
807
|
...payload,
|
|
808
|
+
timing,
|
|
626
809
|
finalState: resolveWorkflowStateSnapshot(cwd)
|
|
627
|
-
} :
|
|
810
|
+
} : {
|
|
811
|
+
...payload,
|
|
812
|
+
timing
|
|
813
|
+
};
|
|
628
814
|
return {
|
|
629
815
|
schemaVersion: 1,
|
|
630
816
|
kind: "treeseed.workflow.result",
|
|
@@ -642,31 +828,108 @@ function buildWorkflowResult(operation, cwd, payload, options = {}) {
|
|
|
642
828
|
errors: options.errors ?? []
|
|
643
829
|
};
|
|
644
830
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
831
|
+
function parseGitStatusChangedPaths(status) {
|
|
832
|
+
return status.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => {
|
|
833
|
+
const value = line.slice(3).trim();
|
|
834
|
+
return value.includes(" -> ") ? value.split(" -> ").at(-1).trim() : value;
|
|
835
|
+
}).filter(Boolean);
|
|
836
|
+
}
|
|
837
|
+
function availableWorkflowAppIds(root) {
|
|
838
|
+
try {
|
|
839
|
+
const ids = discoverTreeseedApplications(root).map((app) => app.id);
|
|
840
|
+
return ids.length > 0 ? ids : ["web"];
|
|
841
|
+
} catch {
|
|
842
|
+
return ["web"];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function selectWorkflowApplications(root, input = {}) {
|
|
846
|
+
const available = availableWorkflowAppIds(root);
|
|
847
|
+
const availableSet = new Set(available);
|
|
848
|
+
const selected = /* @__PURE__ */ new Set();
|
|
849
|
+
const reasons = [];
|
|
850
|
+
const add = (appId, reason) => {
|
|
851
|
+
if (!availableSet.has(appId)) return;
|
|
852
|
+
selected.add(appId);
|
|
853
|
+
reasons.push({ appId, reason });
|
|
854
|
+
};
|
|
855
|
+
const packages = [
|
|
856
|
+
...input.packageSelection?.selected ?? [],
|
|
857
|
+
...input.packageSelection?.changed ?? [],
|
|
858
|
+
...input.packageSelection?.dependents ?? []
|
|
859
|
+
];
|
|
860
|
+
const appByPackage = new Map(discoverTreeseedApplications(root).filter((app) => app.relativeRoot.startsWith("packages/")).map((app) => [`@treeseed/${app.relativeRoot.slice("packages/".length).split("/")[0]}`, app.id]));
|
|
861
|
+
for (const packageName of packages) {
|
|
862
|
+
if (packageName === "@treeseed/api" || packageName === "@treeseed/treedx" || packageName === "@treeseed/agent") {
|
|
863
|
+
add("api", `${packageName} changed`);
|
|
864
|
+
} else if (packageName === "@treeseed/core" || packageName === "@treeseed/ui" || packageName === "@treeseed/admin") {
|
|
865
|
+
add("web", `${packageName} changed`);
|
|
866
|
+
} else if (packageName === "@treeseed/sdk" || packageName === "@treeseed/cli") {
|
|
867
|
+
add("web", `${packageName} is shared`);
|
|
868
|
+
add("api", `${packageName} is shared`);
|
|
869
|
+
}
|
|
870
|
+
const packageAppId = appByPackage.get(packageName);
|
|
871
|
+
if (packageAppId) add(packageAppId, `${packageName} owns ${packageAppId}`);
|
|
872
|
+
}
|
|
873
|
+
const changedPaths = input.changedPaths ?? parseGitStatusChangedPaths(gitStatusPorcelain(root));
|
|
874
|
+
const appByPackagePath = new Map(discoverTreeseedApplications(root).filter((app) => app.relativeRoot.startsWith("packages/")).map((app) => [app.relativeRoot, app.id]));
|
|
875
|
+
for (const file of changedPaths) {
|
|
876
|
+
if (file.startsWith("packages/api/") || file === "packages/api") {
|
|
877
|
+
add("api", `${file} is API-owned`);
|
|
878
|
+
} else if (file.startsWith("packages/treedx/") || file === "packages/treedx") {
|
|
879
|
+
add("api", `${file} is TreeDX implementation`);
|
|
880
|
+
} else if (file.startsWith("packages/core/") || file.startsWith("packages/ui/") || file.startsWith("packages/admin/") || file.startsWith("src/") || file.startsWith("content/") || file.startsWith("public/") || file === "treeseed.site.yaml") {
|
|
881
|
+
add("web", `${file} is web-owned`);
|
|
882
|
+
} else if (file.startsWith("packages/sdk/") || file.startsWith("packages/cli/") || file === "package.json" || file === "package-lock.json" || file.startsWith(".github/")) {
|
|
883
|
+
add("web", `${file} is shared workflow/config`);
|
|
884
|
+
add("api", `${file} is shared workflow/config`);
|
|
885
|
+
}
|
|
886
|
+
for (const [packageRoot, appId] of appByPackagePath) {
|
|
887
|
+
if (file === packageRoot || file.startsWith(`${packageRoot}/`)) {
|
|
888
|
+
add(appId, `${file} is ${appId}-owned`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
648
891
|
}
|
|
649
|
-
|
|
650
|
-
const
|
|
892
|
+
const source = packages.length > 0 ? "package-selection" : changedPaths.length > 0 ? "changed-paths" : "default";
|
|
893
|
+
const finalSelected = selected.size > 0 ? available.filter((appId) => selected.has(appId)) : available;
|
|
894
|
+
return {
|
|
895
|
+
selected: finalSelected,
|
|
896
|
+
skipped: available.filter((appId) => !finalSelected.includes(appId)).map((appId) => ({ appId, reason: "No changed files or selected packages target this application." })),
|
|
897
|
+
reasons,
|
|
898
|
+
source
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function singleSelectedWorkflowAppId(selection) {
|
|
902
|
+
return selection.selected.length === 1 ? selection.selected[0] : void 0;
|
|
903
|
+
}
|
|
904
|
+
async function workflowHostedVerificationGateRequired(operation, root, helpers, environment, options = { enabled: true }) {
|
|
905
|
+
if (!options.enabled) return null;
|
|
906
|
+
const target = environment === "prod" ? "prod" : environment === "local" ? "local" : "staging";
|
|
907
|
+
const readiness = collectTreeseedDeploymentReadiness({
|
|
651
908
|
tenantRoot: root,
|
|
652
|
-
environment,
|
|
653
|
-
|
|
654
|
-
env: helpers.context.env,
|
|
655
|
-
write: (line) => helpers.write(line)
|
|
909
|
+
environment: target,
|
|
910
|
+
appId: options.appId
|
|
656
911
|
});
|
|
657
|
-
if (options.strict && !
|
|
658
|
-
|
|
659
|
-
|
|
912
|
+
if (options.strict && !readiness.ok) {
|
|
913
|
+
const failures = readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
|
|
914
|
+
workflowError(operation, "validation_failed", `Deployment readiness failed for ${target}:
|
|
915
|
+
${failures.join("\n")}`, {
|
|
916
|
+
details: { readiness }
|
|
660
917
|
});
|
|
661
918
|
}
|
|
662
|
-
|
|
919
|
+
workflowError(operation, "validation_failed", `Hosted live verification for ${target} is reconciler-owned. Use stage/release release-gate:hosted-reconcile and release-gate:live-verify resources, or run trsd reconcile verify with a hosted selector.`, {
|
|
920
|
+
details: {
|
|
921
|
+
readiness,
|
|
922
|
+
live: options.live === true,
|
|
923
|
+
appId: options.appId ?? null
|
|
924
|
+
}
|
|
925
|
+
});
|
|
663
926
|
}
|
|
664
927
|
function normalizeExecutionMode(input) {
|
|
665
928
|
return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
|
|
666
929
|
}
|
|
667
930
|
function submodulePointerForRef(repoDir, ref, relativeDir) {
|
|
668
931
|
try {
|
|
669
|
-
const output =
|
|
932
|
+
const output = runGit(["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
|
|
670
933
|
if (!output) {
|
|
671
934
|
return null;
|
|
672
935
|
}
|
|
@@ -710,7 +973,8 @@ function writeJsonFile(path, value) {
|
|
|
710
973
|
`, "utf8");
|
|
711
974
|
}
|
|
712
975
|
function applyStableWorkspaceVersionChanges(root, versions) {
|
|
713
|
-
const
|
|
976
|
+
const dependencyVersions = installableInternalDependencyVersions(root, versions);
|
|
977
|
+
const stableGitReferences = stablePackageGitReferences(root, dependencyVersions);
|
|
714
978
|
for (const target of [{ name: "@treeseed/market", dir: root }, ...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))]) {
|
|
715
979
|
const packageJsonPath = resolve(target.dir, "package.json");
|
|
716
980
|
if (!existsSync(packageJsonPath)) continue;
|
|
@@ -724,7 +988,7 @@ function applyStableWorkspaceVersionChanges(root, versions) {
|
|
|
724
988
|
for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
|
|
725
989
|
const values = packageJson[field];
|
|
726
990
|
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
727
|
-
for (const [dependencyName, version] of
|
|
991
|
+
for (const [dependencyName, version] of dependencyVersions.entries()) {
|
|
728
992
|
if (!(dependencyName in values)) continue;
|
|
729
993
|
const dependencySpec = stableGitReferences.get(dependencyName) ?? version;
|
|
730
994
|
if (String(values[dependencyName]) === dependencySpec) continue;
|
|
@@ -753,13 +1017,13 @@ function stablePackageGitReferences(root, versions) {
|
|
|
753
1017
|
}
|
|
754
1018
|
function gitObjectCommit(repoDir, ref) {
|
|
755
1019
|
try {
|
|
756
|
-
return
|
|
1020
|
+
return runGit(["rev-list", "-n", "1", ref], { cwd: repoDir, capture: true }).trim() || null;
|
|
757
1021
|
} catch {
|
|
758
1022
|
return null;
|
|
759
1023
|
}
|
|
760
1024
|
}
|
|
761
1025
|
function remoteTagCommit(repoDir, tagName) {
|
|
762
|
-
const output =
|
|
1026
|
+
const output = runGit(["ls-remote", "origin", `refs/tags/${tagName}`, `refs/tags/${tagName}^{}`], { cwd: repoDir, capture: true }).trim();
|
|
763
1027
|
if (!output) return null;
|
|
764
1028
|
const peeled = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
|
|
765
1029
|
const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
|
|
@@ -771,14 +1035,14 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
|
771
1035
|
throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
|
|
772
1036
|
}
|
|
773
1037
|
if (!localCommit) {
|
|
774
|
-
|
|
1038
|
+
runGit(["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
|
|
775
1039
|
}
|
|
776
1040
|
const remoteCommit = remoteTagCommit(repoDir, tagName);
|
|
777
1041
|
if (remoteCommit && remoteCommit !== commitSha) {
|
|
778
1042
|
throw new Error(`Release tag ${tagName} already exists on origin at ${remoteCommit}, expected ${commitSha}.`);
|
|
779
1043
|
}
|
|
780
1044
|
if (!remoteCommit) {
|
|
781
|
-
|
|
1045
|
+
runGit(["push", "origin", tagName], { cwd: repoDir });
|
|
782
1046
|
}
|
|
783
1047
|
return {
|
|
784
1048
|
tagName,
|
|
@@ -786,12 +1050,28 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
|
786
1050
|
remote: remoteCommit ? "existing" : "pushed"
|
|
787
1051
|
};
|
|
788
1052
|
}
|
|
1053
|
+
function promoteCommitToProductionBranch(repoDir, commitSha) {
|
|
1054
|
+
const expectedBefore = remoteBranchExists(repoDir, PRODUCTION_BRANCH) ? remoteHeadCommit(repoDir, PRODUCTION_BRANCH) : null;
|
|
1055
|
+
const lease = expectedBefore ? `--force-with-lease=refs/heads/${PRODUCTION_BRANCH}:${expectedBefore}` : "--force-with-lease";
|
|
1056
|
+
runGit(["push", lease, "origin", `${commitSha}:refs/heads/${PRODUCTION_BRANCH}`], { cwd: repoDir });
|
|
1057
|
+
const observed = remoteHeadCommit(repoDir, PRODUCTION_BRANCH);
|
|
1058
|
+
if (observed !== commitSha) {
|
|
1059
|
+
throw new Error(`Production promotion verification failed; expected ${commitSha}, observed ${observed}.`);
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
1063
|
+
expectedBefore,
|
|
1064
|
+
commitSha,
|
|
1065
|
+
pushed: true,
|
|
1066
|
+
verified: true
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
789
1069
|
function commitAllIfChanged(repoDir, message) {
|
|
790
|
-
|
|
1070
|
+
runGit(["add", "-A"], { cwd: repoDir });
|
|
791
1071
|
if (!hasMeaningfulChanges(repoDir)) {
|
|
792
1072
|
return { committed: false, commitSha: headCommit(repoDir) };
|
|
793
1073
|
}
|
|
794
|
-
|
|
1074
|
+
runGit(["commit", "-m", message], { cwd: repoDir });
|
|
795
1075
|
return { committed: true, commitSha: headCommit(repoDir) };
|
|
796
1076
|
}
|
|
797
1077
|
function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
|
|
@@ -869,6 +1149,11 @@ function defaultCiWorkflows(kind, branch) {
|
|
|
869
1149
|
}
|
|
870
1150
|
return ["verify.yml"];
|
|
871
1151
|
}
|
|
1152
|
+
function packageCiWorkflowsForRepo(repoDir) {
|
|
1153
|
+
const adapters = discoverTreeseedPackageAdapters(workspaceRoot(repoDir));
|
|
1154
|
+
const adapter = adapters.find((candidate) => resolve(candidate.dir) === resolve(repoDir));
|
|
1155
|
+
return adapter ? [hostedWorkflowForPackage(adapter)] : null;
|
|
1156
|
+
}
|
|
872
1157
|
function githubRepositoryForRepo(repoDir) {
|
|
873
1158
|
try {
|
|
874
1159
|
return resolveGitHubRepositorySlug(repoDir);
|
|
@@ -878,7 +1163,7 @@ function githubRepositoryForRepo(repoDir) {
|
|
|
878
1163
|
}
|
|
879
1164
|
function ciTargetForRepo(repo, kind, input, workflowOverrides) {
|
|
880
1165
|
const branch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : repo.branchName;
|
|
881
|
-
const workflows = workflowOverrides.length > 0 ? workflowOverrides : defaultCiWorkflows(kind, branch);
|
|
1166
|
+
const workflows = workflowOverrides.length > 0 ? workflowOverrides : kind === "package" ? packageCiWorkflowsForRepo(repo.path) ?? defaultCiWorkflows(kind, branch) : defaultCiWorkflows(kind, branch);
|
|
882
1167
|
return {
|
|
883
1168
|
name: repo.name,
|
|
884
1169
|
repoPath: repo.path,
|
|
@@ -931,7 +1216,7 @@ async function createCiResult(cwd, input) {
|
|
|
931
1216
|
function createTasksResult(cwd) {
|
|
932
1217
|
const tenantRoot = cwd;
|
|
933
1218
|
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
934
|
-
const deployConfig =
|
|
1219
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
|
|
935
1220
|
const dirty = gitStatusPorcelain(repoDir).length > 0;
|
|
936
1221
|
const tasks = listTaskBranches(repoDir).map((branch) => {
|
|
937
1222
|
const previewState = loadDeployState(tenantRoot, deployConfig, {
|
|
@@ -1064,7 +1349,7 @@ async function connectTreeseedMarketProject(helpers, tenantRoot, input, context)
|
|
|
1064
1349
|
principal: activeRemoteSession?.principal ?? null
|
|
1065
1350
|
});
|
|
1066
1351
|
}
|
|
1067
|
-
const runnerHostId = `
|
|
1352
|
+
const runnerHostId = `operations-runner:${projectId}`;
|
|
1068
1353
|
if (connectionResult.runnerToken) {
|
|
1069
1354
|
setTreeseedRemoteSession(tenantRoot, {
|
|
1070
1355
|
hostId: runnerHostId,
|
|
@@ -1180,16 +1465,61 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
1180
1465
|
if (branch === STAGING_BRANCH && (hasMeaningfulChanges(repoRoot(root)) || checkedOutWorkspacePackageRepos(root).some((repo) => hasMeaningfulChanges(repo.dir)))) {
|
|
1181
1466
|
return null;
|
|
1182
1467
|
}
|
|
1183
|
-
|
|
1468
|
+
const currentHeads = Object.fromEntries([
|
|
1469
|
+
["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
|
|
1470
|
+
...checkedOutWorkspacePackageRepos(root).map((repo) => [
|
|
1471
|
+
repo.name,
|
|
1472
|
+
runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
|
|
1473
|
+
])
|
|
1474
|
+
]);
|
|
1475
|
+
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
1476
|
+
if (journal.command !== "save" || !journal.resumable || journal.session.branchName !== branch) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1480
|
+
currentBranch: branch,
|
|
1481
|
+
currentHeads
|
|
1482
|
+
});
|
|
1483
|
+
if (classification.state === "resumable") {
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
if (classification.state === "stale") {
|
|
1487
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1488
|
+
...classification,
|
|
1489
|
+
reasons: ["save auto-resume skipped stale failed save", ...classification.reasons]
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
return false;
|
|
1493
|
+
}) ?? null;
|
|
1494
|
+
}
|
|
1495
|
+
function workflowFileExists(repoPath, workflow) {
|
|
1496
|
+
return existsSync(resolve(repoPath, ".github", "workflows", workflow));
|
|
1497
|
+
}
|
|
1498
|
+
function hostedWorkflowForSavedRepository(root, repo) {
|
|
1499
|
+
const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
|
|
1500
|
+
const adapterWorkflow = packageHostedVerifyWorkflow(adapterByPath.get(resolve(repo.path)));
|
|
1501
|
+
if (adapterWorkflow) return adapterWorkflow;
|
|
1502
|
+
if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
|
|
1503
|
+
return "deploy.yml";
|
|
1504
|
+
}
|
|
1505
|
+
return "verify.yml";
|
|
1184
1506
|
}
|
|
1185
|
-
function
|
|
1186
|
-
return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) =>
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1507
|
+
function gatesForSavedRepositoryReports(root, reports) {
|
|
1508
|
+
return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch && (repo.committed || repo.tagName)).map((repo) => {
|
|
1509
|
+
const workflow = hostedWorkflowForSavedRepository(root, repo);
|
|
1510
|
+
const gate = {
|
|
1511
|
+
name: repo.name,
|
|
1512
|
+
repoPath: repo.path,
|
|
1513
|
+
workflow,
|
|
1514
|
+
branch: String(repo.branch),
|
|
1515
|
+
headSha: String(repo.commitSha)
|
|
1516
|
+
};
|
|
1517
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
function packageHostedVerifyWorkflow(adapter) {
|
|
1521
|
+
const workflow = adapter?.metadata?.hostedVerifyWorkflow;
|
|
1522
|
+
return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
|
|
1193
1523
|
}
|
|
1194
1524
|
function gateForSavedRootReport(report, branch, scope) {
|
|
1195
1525
|
if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
|
|
@@ -1254,7 +1584,7 @@ function generatedReleaseMetadataFiles(repoDir) {
|
|
|
1254
1584
|
return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
|
|
1255
1585
|
if (existsSync(resolve(repoDir, filePath))) return true;
|
|
1256
1586
|
try {
|
|
1257
|
-
|
|
1587
|
+
runGit(["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
|
|
1258
1588
|
return true;
|
|
1259
1589
|
} catch {
|
|
1260
1590
|
return false;
|
|
@@ -1283,12 +1613,12 @@ function restoreReleaseGeneratedMetadata(repo) {
|
|
|
1283
1613
|
const restored = [];
|
|
1284
1614
|
const skipped = [];
|
|
1285
1615
|
for (const filePath of repo.files) {
|
|
1286
|
-
const status =
|
|
1616
|
+
const status = runGit(["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
|
|
1287
1617
|
if (!status.trim()) {
|
|
1288
1618
|
skipped.push(filePath);
|
|
1289
1619
|
continue;
|
|
1290
1620
|
}
|
|
1291
|
-
|
|
1621
|
+
runGit(["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
|
|
1292
1622
|
restored.push(filePath);
|
|
1293
1623
|
}
|
|
1294
1624
|
return { restored, skipped };
|
|
@@ -1525,7 +1855,12 @@ function failWorkflowRun(root, runId, error, recovery) {
|
|
|
1525
1855
|
releaseWorkflowLock(root, runId);
|
|
1526
1856
|
}
|
|
1527
1857
|
function validatePackageReleaseWorkflows(root, packageNames) {
|
|
1528
|
-
const
|
|
1858
|
+
const adapterById = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [adapter.id, adapter]));
|
|
1859
|
+
const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => {
|
|
1860
|
+
const adapter = adapterById.get(pkg.name);
|
|
1861
|
+
const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
|
|
1862
|
+
return !existsSync(resolve(pkg.dir, workflow));
|
|
1863
|
+
}).map((pkg) => pkg.name);
|
|
1529
1864
|
if (missing.length > 0) {
|
|
1530
1865
|
workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
|
|
1531
1866
|
details: {
|
|
@@ -1534,6 +1869,26 @@ function validatePackageReleaseWorkflows(root, packageNames) {
|
|
|
1534
1869
|
});
|
|
1535
1870
|
}
|
|
1536
1871
|
}
|
|
1872
|
+
function releaseWorkflowForPackage(root, packageName) {
|
|
1873
|
+
const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === packageName || entry.name === packageName);
|
|
1874
|
+
const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
|
|
1875
|
+
return workflow.split("/").at(-1) || "publish.yml";
|
|
1876
|
+
}
|
|
1877
|
+
function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
1878
|
+
const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
|
|
1879
|
+
if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
|
|
1880
|
+
run("tsx", ["scripts/bump-release-version.ts", version], { cwd: pkg.dir });
|
|
1881
|
+
return { status: "updated", adapter: adapter.id, command: "tsx scripts/bump-release-version.ts" };
|
|
1882
|
+
}
|
|
1883
|
+
if (existsSync(resolve(pkg.dir, "package.json"))) {
|
|
1884
|
+
return {
|
|
1885
|
+
status: "npm-install",
|
|
1886
|
+
adapter: adapter?.id ?? pkg.name,
|
|
1887
|
+
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
|
|
1891
|
+
}
|
|
1537
1892
|
function validateStagingWorkflowContracts(root) {
|
|
1538
1893
|
if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
1539
1894
|
return;
|
|
@@ -1553,17 +1908,38 @@ function validateStagingWorkflowContracts(root) {
|
|
|
1553
1908
|
function shouldSkipReleaseInstall() {
|
|
1554
1909
|
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
|
|
1555
1910
|
}
|
|
1911
|
+
function npmCommandForWorkflowSpawn(args) {
|
|
1912
|
+
if (process.platform === "win32") {
|
|
1913
|
+
return { command: "npm", args };
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
command: "bash",
|
|
1917
|
+
args: [
|
|
1918
|
+
"-lc",
|
|
1919
|
+
'ulimit -n 65535 2>/dev/null || ulimit -n 32768 2>/dev/null || ulimit -n 16384 2>/dev/null || true; exec npm "$@"',
|
|
1920
|
+
"npm-fd-guard",
|
|
1921
|
+
...args
|
|
1922
|
+
]
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1556
1925
|
function runReleaseNpmInstall(repoDir, options = {}) {
|
|
1557
1926
|
if (shouldSkipReleaseInstall()) {
|
|
1558
1927
|
return { status: "skipped", reason: "disabled" };
|
|
1559
1928
|
}
|
|
1560
1929
|
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
1561
|
-
const
|
|
1930
|
+
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
1931
|
+
const result = spawnSync(spawnCommand.command, spawnCommand.args, {
|
|
1562
1932
|
cwd: repoDir,
|
|
1563
1933
|
env: {
|
|
1564
1934
|
...process.env,
|
|
1565
1935
|
npm_config_audit: "false",
|
|
1566
|
-
|
|
1936
|
+
npm_config_fetch_retries: "2",
|
|
1937
|
+
npm_config_fund: "false",
|
|
1938
|
+
npm_config_foreground_scripts: "true",
|
|
1939
|
+
npm_config_loglevel: "warn",
|
|
1940
|
+
npm_config_maxsockets: "4",
|
|
1941
|
+
npm_config_prefer_offline: "true",
|
|
1942
|
+
npm_config_progress: "false"
|
|
1567
1943
|
},
|
|
1568
1944
|
stdio: "pipe",
|
|
1569
1945
|
encoding: "utf8"
|
|
@@ -1598,7 +1974,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
|
|
|
1598
1974
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
1599
1975
|
checkoutBranch(repoDir, STAGING_BRANCH);
|
|
1600
1976
|
try {
|
|
1601
|
-
|
|
1977
|
+
runGit(["merge-base", "--is-ancestor", `origin/${PRODUCTION_BRANCH}`, "HEAD"], { cwd: repoDir, capture: true });
|
|
1602
1978
|
return {
|
|
1603
1979
|
status: "up-to-date",
|
|
1604
1980
|
merged: false,
|
|
@@ -1610,7 +1986,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
|
|
|
1610
1986
|
} catch {
|
|
1611
1987
|
}
|
|
1612
1988
|
try {
|
|
1613
|
-
|
|
1989
|
+
runGit(["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
|
|
1614
1990
|
} catch (error) {
|
|
1615
1991
|
const report = collectMergeConflictReport(repoDir);
|
|
1616
1992
|
throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
@@ -1663,69 +2039,6 @@ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, optio
|
|
|
1663
2039
|
packageStagingPointerCommit: pointerSync.commitSha
|
|
1664
2040
|
};
|
|
1665
2041
|
}
|
|
1666
|
-
function collectActiveDevTagReferences(root) {
|
|
1667
|
-
return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
|
|
1668
|
-
}
|
|
1669
|
-
function normalizeIncludePackages(value) {
|
|
1670
|
-
if (!value) return null;
|
|
1671
|
-
const values = Array.isArray(value) ? value : String(value).split(",");
|
|
1672
|
-
const normalized = values.map((entry) => String(entry).trim()).filter(Boolean);
|
|
1673
|
-
return normalized.length > 0 ? new Set(normalized) : null;
|
|
1674
|
-
}
|
|
1675
|
-
function normalizeDevTagBranchScope(value) {
|
|
1676
|
-
return value === "staging" || value === "preview" || value === "all" ? value : "all";
|
|
1677
|
-
}
|
|
1678
|
-
function packageJsonVersion(repoDir) {
|
|
1679
|
-
const packageJson = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
|
|
1680
|
-
return String(packageJson.version ?? "");
|
|
1681
|
-
}
|
|
1682
|
-
function collectStaleDevTagCleanupReports(root, input, options) {
|
|
1683
|
-
const includePackages = normalizeIncludePackages(input.includePackages);
|
|
1684
|
-
const branchScope = normalizeDevTagBranchScope(input.branchScope);
|
|
1685
|
-
const activeDevTags = collectActiveDevTagReferences(root);
|
|
1686
|
-
const repos = checkedOutWorkspacePackageRepos(root).filter((pkg) => !includePackages || includePackages.has(pkg.name)).map((pkg) => {
|
|
1687
|
-
const currentVersion = packageJsonVersion(pkg.dir);
|
|
1688
|
-
const report = options.execute ? cleanupStaleTreeseedDevTags({
|
|
1689
|
-
repoDir: pkg.dir,
|
|
1690
|
-
packageName: pkg.name,
|
|
1691
|
-
currentVersion,
|
|
1692
|
-
activeReferences: activeDevTags,
|
|
1693
|
-
branchScope
|
|
1694
|
-
}) : {
|
|
1695
|
-
...collectTreeseedDevTagCleanupPlan({
|
|
1696
|
-
repoDir: pkg.dir,
|
|
1697
|
-
packageName: pkg.name,
|
|
1698
|
-
currentVersion,
|
|
1699
|
-
activeReferences: activeDevTags,
|
|
1700
|
-
branchScope
|
|
1701
|
-
}),
|
|
1702
|
-
status: "planned",
|
|
1703
|
-
candidateCount: 0,
|
|
1704
|
-
cleaned: [],
|
|
1705
|
-
cleanedCount: 0,
|
|
1706
|
-
skippedCount: 0
|
|
1707
|
-
};
|
|
1708
|
-
if (!options.execute) {
|
|
1709
|
-
const planned = report;
|
|
1710
|
-
planned.candidateCount = planned.candidates.length;
|
|
1711
|
-
planned.skippedCount = planned.skipped.length;
|
|
1712
|
-
}
|
|
1713
|
-
return {
|
|
1714
|
-
name: pkg.name,
|
|
1715
|
-
path: relative(root, pkg.dir),
|
|
1716
|
-
...report
|
|
1717
|
-
};
|
|
1718
|
-
});
|
|
1719
|
-
return {
|
|
1720
|
-
status: options.execute ? "completed" : "planned",
|
|
1721
|
-
branchScope,
|
|
1722
|
-
includePackages: includePackages ? [...includePackages].sort() : [],
|
|
1723
|
-
repos,
|
|
1724
|
-
candidateCount: repos.reduce((total, repo) => total + Number(repo.candidateCount ?? 0), 0),
|
|
1725
|
-
cleanedCount: repos.reduce((total, repo) => total + Number(repo.cleanedCount ?? 0), 0),
|
|
1726
|
-
skippedCount: repos.reduce((total, repo) => total + Number(repo.skippedCount ?? 0), 0)
|
|
1727
|
-
};
|
|
1728
|
-
}
|
|
1729
2042
|
function releasePlanVersionMap(plannedVersions) {
|
|
1730
2043
|
return new Map(
|
|
1731
2044
|
Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
|
|
@@ -1757,36 +2070,233 @@ function stableDependencyVersionsForReleaseLine(root, options) {
|
|
|
1757
2070
|
}
|
|
1758
2071
|
return versions;
|
|
1759
2072
|
}
|
|
1760
|
-
function
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
}
|
|
1764
|
-
const rendered = report.failures.map((failure) => `- ${failure.scope}${failure.provider ? ` ${failure.provider}` : ""}: ${failure.message}`);
|
|
1765
|
-
workflowError(operation, "validation_failed", [
|
|
1766
|
-
"Treeseed release-candidate readiness failed.",
|
|
1767
|
-
...rendered
|
|
1768
|
-
].join("\n"), {
|
|
1769
|
-
details: {
|
|
1770
|
-
releaseCandidate: report
|
|
1771
|
-
}
|
|
1772
|
-
});
|
|
2073
|
+
function releaseCandidateProofDriver(mode, lane = "fast") {
|
|
2074
|
+
if (lane === "promotion" || mode === "strict") return "github-hosted";
|
|
2075
|
+
return "local";
|
|
1773
2076
|
}
|
|
1774
|
-
async function
|
|
1775
|
-
const plannedVersions = plannedRelease.plannedVersions && typeof plannedRelease.plannedVersions === "object" && !Array.isArray(plannedRelease.plannedVersions) ? plannedRelease.plannedVersions : {};
|
|
1776
|
-
const stableDependencyVersions = plannedRelease.stableDependencyVersions && typeof plannedRelease.stableDependencyVersions === "object" && !Array.isArray(plannedRelease.stableDependencyVersions) ? plannedRelease.stableDependencyVersions : {};
|
|
2077
|
+
async function runReleaseCandidateProofForPlan(operation, root, plannedRelease, options = {}) {
|
|
1777
2078
|
const packageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
|
|
1778
|
-
const
|
|
2079
|
+
const mode = options.mode ?? normalizeReleaseCandidateMode(void 0, operation);
|
|
2080
|
+
const driver = releaseCandidateProofDriver(mode, options.lane ?? "fast");
|
|
2081
|
+
const proof = await runTreeseedProof({
|
|
1779
2082
|
root,
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2083
|
+
target: operation === "release" ? "prod" : "staging",
|
|
2084
|
+
driver,
|
|
2085
|
+
write: options.write
|
|
1783
2086
|
});
|
|
1784
|
-
|
|
1785
|
-
|
|
2087
|
+
if (proof.failures.length > 0) {
|
|
2088
|
+
const first = proof.failures[0];
|
|
2089
|
+
workflowError(operation, "validation_failed", [
|
|
2090
|
+
"Treeseed release-candidate proof failed.",
|
|
2091
|
+
`- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
|
|
2092
|
+
first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
|
|
2093
|
+
driver === "github-hosted" ? "Hosted GitHub workflow proof is authoritative; local action simulation is advisory." : "Local proof is exact-input cached in the proof ledger and reruns only missing or invalid subjects."
|
|
2094
|
+
].filter(Boolean).join("\n"), { details: { proof } });
|
|
2095
|
+
}
|
|
2096
|
+
return {
|
|
2097
|
+
mode,
|
|
2098
|
+
driver,
|
|
2099
|
+
selectedPackageNames: packageSelection.selected,
|
|
2100
|
+
proof,
|
|
2101
|
+
status: "passed",
|
|
2102
|
+
reused: proof.reused.length,
|
|
2103
|
+
records: proof.records.length
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function parseProofOlderThan(value) {
|
|
2107
|
+
if (!value) return 30 * 24 * 60 * 60 * 1e3;
|
|
2108
|
+
const match = value.trim().match(/^(\d+)([smhd])?$/u);
|
|
2109
|
+
if (!match) return 30 * 24 * 60 * 60 * 1e3;
|
|
2110
|
+
const amount = Number(match[1]);
|
|
2111
|
+
const unit = match[2] ?? "d";
|
|
2112
|
+
if (!Number.isFinite(amount) || amount < 0) return 30 * 24 * 60 * 60 * 1e3;
|
|
2113
|
+
if (unit === "s") return amount * 1e3;
|
|
2114
|
+
if (unit === "m") return amount * 60 * 1e3;
|
|
2115
|
+
if (unit === "h") return amount * 60 * 60 * 1e3;
|
|
2116
|
+
return amount * 24 * 60 * 60 * 1e3;
|
|
2117
|
+
}
|
|
2118
|
+
async function workflowProof(helpers, input = {}) {
|
|
2119
|
+
try {
|
|
2120
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
2121
|
+
const timer = createTreeseedWorkflowTimer();
|
|
2122
|
+
const tenantRoot = resolveProjectRootOrThrow("proof", helpers.cwd());
|
|
2123
|
+
const root = workspaceRoot(tenantRoot);
|
|
2124
|
+
const action = input.action ?? (input.plan || input.dryRun ? "plan" : "status");
|
|
2125
|
+
const target = input.target ?? "staging";
|
|
2126
|
+
const driver = input.driver ?? "github-hosted";
|
|
2127
|
+
const executionMode = action === "plan" || input.plan || input.dryRun ? "plan" : "execute";
|
|
2128
|
+
let payload;
|
|
2129
|
+
if (action === "run") {
|
|
2130
|
+
const result = await timer.phaseAsync("proof-run", "Run release proof subjects", () => runTreeseedProof({
|
|
2131
|
+
root,
|
|
2132
|
+
target,
|
|
2133
|
+
driver,
|
|
2134
|
+
subject: input.subject ?? null,
|
|
2135
|
+
write: (line, stream) => helpers.write(line, stream)
|
|
2136
|
+
}));
|
|
2137
|
+
payload = {
|
|
2138
|
+
action,
|
|
2139
|
+
target,
|
|
2140
|
+
driver,
|
|
2141
|
+
subject: input.subject ?? null,
|
|
2142
|
+
...result,
|
|
2143
|
+
authority: driver === "github-hosted" ? "authoritative" : "advisory"
|
|
2144
|
+
};
|
|
2145
|
+
if (result.failures.length > 0) {
|
|
2146
|
+
const first = result.failures[0];
|
|
2147
|
+
workflowError("proof", "validation_failed", [
|
|
2148
|
+
"Treeseed release proof failed.",
|
|
2149
|
+
`- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
|
|
2150
|
+
first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
|
|
2151
|
+
"Hosted GitHub workflow proof is authoritative; local action simulation is advisory."
|
|
2152
|
+
].filter(Boolean).join("\n"), { details: { proof: payload } });
|
|
2153
|
+
}
|
|
2154
|
+
} else if (action === "clean") {
|
|
2155
|
+
payload = {
|
|
2156
|
+
action,
|
|
2157
|
+
target,
|
|
2158
|
+
...timer.phase("proof-clean", "Clean old proof records", () => cleanProofLedger(root, {
|
|
2159
|
+
olderThanMs: parseProofOlderThan(input.olderThan)
|
|
2160
|
+
}))
|
|
2161
|
+
};
|
|
2162
|
+
} else if (action === "failures") {
|
|
2163
|
+
const ledger = timer.phase("proof-failures", "Inspect failed proof records", () => summarizeTreeseedProofLedger(root));
|
|
2164
|
+
payload = { action, target, failures: ledger.failures, summary: ledger.summary };
|
|
2165
|
+
} else if (action === "explain") {
|
|
2166
|
+
const ledger = timer.phase("proof-explain", "Explain proof duration and reuse", () => summarizeTreeseedProofLedger(root));
|
|
2167
|
+
payload = {
|
|
2168
|
+
action,
|
|
2169
|
+
target,
|
|
2170
|
+
latest: ledger.latest,
|
|
2171
|
+
slowest: ledger.slowest,
|
|
2172
|
+
reuse: {
|
|
2173
|
+
passed: ledger.summary.reusable,
|
|
2174
|
+
rerun: Math.max(0, ledger.summary.records - ledger.summary.reusable),
|
|
2175
|
+
blocked: ledger.summary.failed
|
|
2176
|
+
},
|
|
2177
|
+
summary: ledger.summary
|
|
2178
|
+
};
|
|
2179
|
+
} else {
|
|
2180
|
+
const plan = timer.phase("proof-plan", "Plan release proof subjects", () => buildTreeseedProofPlan({
|
|
2181
|
+
root,
|
|
2182
|
+
target,
|
|
2183
|
+
driver,
|
|
2184
|
+
subject: input.subject ?? null
|
|
2185
|
+
}));
|
|
2186
|
+
payload = { action, target, driver, plan, authority: driver === "github-hosted" ? "authoritative" : "advisory" };
|
|
2187
|
+
}
|
|
2188
|
+
const timing = timer.finish();
|
|
2189
|
+
return buildWorkflowResult("proof", root, payload, {
|
|
2190
|
+
executionMode,
|
|
2191
|
+
summary: action === "run" ? "Treeseed release proof run completed." : "Treeseed release proof report ready.",
|
|
2192
|
+
timing,
|
|
2193
|
+
nextSteps: createNextSteps([
|
|
2194
|
+
{ operation: "proof", reason: "Run missing authoritative hosted proof before promotion.", input: { action: "run", target, driver: "github-hosted" } }
|
|
2195
|
+
])
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
toError("proof", error);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
function normalizeReleaseCandidatePackages(value) {
|
|
2203
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
2204
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
2205
|
+
return [];
|
|
2206
|
+
}
|
|
2207
|
+
async function workflowReleaseCandidate(helpers, input = {}) {
|
|
2208
|
+
try {
|
|
2209
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
2210
|
+
const timer = createTreeseedWorkflowTimer();
|
|
2211
|
+
const tenantRoot = resolveProjectRootOrThrow("release-candidate", helpers.cwd());
|
|
2212
|
+
const root = workspaceRoot(tenantRoot);
|
|
2213
|
+
const executionMode = normalizeExecutionMode(input);
|
|
2214
|
+
const selectedPackageNames = normalizeReleaseCandidatePackages(input.package);
|
|
2215
|
+
const mode = input.mode ?? "strict";
|
|
2216
|
+
const driver = input.verifyDriver === "local" ? "local" : input.verifyDriver === "action" ? "act" : releaseCandidateProofDriver(mode === "skip" ? "hybrid" : mode);
|
|
2217
|
+
const proofSubject = selectedPackageNames.length === 1 ? `package:${selectedPackageNames[0]}` : null;
|
|
2218
|
+
const plan = timer.phase("proof-plan", "Plan release-candidate proof subjects", () => buildTreeseedProofPlan({
|
|
2219
|
+
root,
|
|
2220
|
+
target: "staging",
|
|
2221
|
+
driver,
|
|
2222
|
+
subject: proofSubject
|
|
2223
|
+
}));
|
|
2224
|
+
const payload = {
|
|
2225
|
+
mode,
|
|
2226
|
+
driver,
|
|
2227
|
+
verifyDriver: input.verifyDriver ?? "auto",
|
|
2228
|
+
selectedPackageNames,
|
|
2229
|
+
keepWorkspace: input.keepWorkspace === true,
|
|
2230
|
+
plan,
|
|
2231
|
+
plannedSteps: [
|
|
2232
|
+
{ id: "proof-plan", description: "Discover reusable exact-input proof records for package subjects." },
|
|
2233
|
+
{ id: "proof-run", description: "Run only missing or invalid release proof nodes." }
|
|
2234
|
+
]
|
|
2235
|
+
};
|
|
2236
|
+
if (executionMode === "plan") {
|
|
2237
|
+
return buildWorkflowResult("release-candidate", root, payload, {
|
|
2238
|
+
executionMode,
|
|
2239
|
+
summary: "Treeseed release-candidate proof plan ready.",
|
|
2240
|
+
nextSteps: createNextSteps([
|
|
2241
|
+
{ operation: "release-candidate", reason: "Run missing proof nodes before promotion." }
|
|
2242
|
+
])
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
if (mode === "skip") {
|
|
2246
|
+
return buildWorkflowResult("release-candidate", root, {
|
|
2247
|
+
...payload,
|
|
2248
|
+
status: "skipped",
|
|
2249
|
+
failures: []
|
|
2250
|
+
}, {
|
|
2251
|
+
summary: "Treeseed release-candidate proof skipped.",
|
|
2252
|
+
timing: timer.finish()
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
const proof = await timer.phaseAsync("proof-run", "Run release-candidate proof nodes", () => runTreeseedProof({
|
|
2256
|
+
root,
|
|
2257
|
+
target: "staging",
|
|
2258
|
+
driver,
|
|
2259
|
+
subject: proofSubject,
|
|
2260
|
+
write: (line, stream) => helpers.write(line, stream)
|
|
2261
|
+
}));
|
|
2262
|
+
const resultPayload = {
|
|
2263
|
+
...payload,
|
|
2264
|
+
proof,
|
|
2265
|
+
failures: proof.failures
|
|
2266
|
+
};
|
|
2267
|
+
if (proof.failures.length > 0) {
|
|
2268
|
+
const first = proof.failures[0];
|
|
2269
|
+
workflowError("release-candidate", "validation_failed", [
|
|
2270
|
+
"Treeseed release-candidate proof failed.",
|
|
2271
|
+
`- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
|
|
2272
|
+
first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null
|
|
2273
|
+
].filter(Boolean).join("\n"), {
|
|
2274
|
+
details: { releaseCandidate: resultPayload }
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
return buildWorkflowResult("release-candidate", root, resultPayload, {
|
|
2278
|
+
summary: "Treeseed release-candidate proof passed.",
|
|
2279
|
+
timing: timer.finish(),
|
|
2280
|
+
nextSteps: createNextSteps([
|
|
2281
|
+
{ operation: "stage", reason: "Run stage after the matching release proof passes." }
|
|
2282
|
+
])
|
|
2283
|
+
});
|
|
2284
|
+
});
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
toError("release-candidate", error);
|
|
2287
|
+
}
|
|
1786
2288
|
}
|
|
1787
2289
|
function buildReleasePlanSnapshot(input) {
|
|
1788
2290
|
const selectedPackageNames = new Set(input.packageSelection.selected);
|
|
2291
|
+
const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
|
|
1789
2292
|
const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
|
|
2293
|
+
if (input.repairVersionLine !== true) {
|
|
2294
|
+
for (const adapter of discoverTreeseedPackageAdapters(input.root)) {
|
|
2295
|
+
if (!selectedPackageNames.has(adapter.id) || versionPlan.versions.has(adapter.id) || !adapter.version) continue;
|
|
2296
|
+
versionPlan.selected.add(adapter.id);
|
|
2297
|
+
versionPlan.versions.set(adapter.id, incrementVersion(adapter.version, input.level));
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
1790
2300
|
const plannedSelected = [...versionPlan.selected].filter((name) => versionPlan.versions.has(name));
|
|
1791
2301
|
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(input.packageSelection.changed.filter((name) => plannedSelected.includes(name))));
|
|
1792
2302
|
const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
|
|
@@ -1821,10 +2331,11 @@ function buildReleasePlanSnapshot(input) {
|
|
|
1821
2331
|
packageSelection: plannedPackageSelection,
|
|
1822
2332
|
plannedVersions,
|
|
1823
2333
|
stableDependencyVersions,
|
|
2334
|
+
applicationSelection,
|
|
1824
2335
|
plannedDevReferenceRewrites,
|
|
1825
2336
|
plannedPublishWaits: plannedPackageSelection.selected.map((name) => ({
|
|
1826
2337
|
name,
|
|
1827
|
-
workflow:
|
|
2338
|
+
workflow: releaseWorkflowForPackage(input.root, name),
|
|
1828
2339
|
branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
|
|
1829
2340
|
status: "planned"
|
|
1830
2341
|
})),
|
|
@@ -1843,7 +2354,6 @@ function buildReleasePlanSnapshot(input) {
|
|
|
1843
2354
|
})),
|
|
1844
2355
|
{ id: "release-root", description: `Release market ${rootVersion}` },
|
|
1845
2356
|
{ id: "release-back-merge", description: "Back-merge production release history into staging" },
|
|
1846
|
-
{ id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
|
|
1847
2357
|
{ id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
|
|
1848
2358
|
],
|
|
1849
2359
|
blockers: input.blockers
|
|
@@ -1884,10 +2394,26 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames, ciMode)
|
|
|
1884
2394
|
if (ciMode === "off") {
|
|
1885
2395
|
return;
|
|
1886
2396
|
}
|
|
1887
|
-
|
|
2397
|
+
const values = resolveTreeseedMachineEnvironmentValues(root, "prod");
|
|
2398
|
+
const missing = [];
|
|
1888
2399
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1889
2400
|
if (!selectedPackageNames.has(pkg.name)) continue;
|
|
1890
|
-
resolveGitHubRepositorySlug(pkg.dir);
|
|
2401
|
+
const repository = resolveGitHubRepositorySlug(pkg.dir);
|
|
2402
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
|
|
2403
|
+
if (!credential.token) {
|
|
2404
|
+
missing.push({ packageName: pkg.name, repository: credential.repository, envName: credential.envName });
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
if (missing.length > 0) {
|
|
2408
|
+
workflowError(
|
|
2409
|
+
"release",
|
|
2410
|
+
"github_auth_unavailable",
|
|
2411
|
+
[
|
|
2412
|
+
"Treeseed release automation requires Treeseed-prefixed GitHub credentials.",
|
|
2413
|
+
...missing.map((pkg) => `- ${pkg.packageName}: configure ${pkg.envName} for ${pkg.repository}, or TREESEED_GITHUB_TOKEN as a fallback.`)
|
|
2414
|
+
].join("\n"),
|
|
2415
|
+
{ details: { missing } }
|
|
2416
|
+
);
|
|
1891
2417
|
}
|
|
1892
2418
|
}
|
|
1893
2419
|
function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
|
|
@@ -1914,31 +2440,31 @@ function assertSessionBranchSafety(operation, session, {
|
|
|
1914
2440
|
requireCurrentBranch = false,
|
|
1915
2441
|
allowPackageReposWithoutOrigin = false
|
|
1916
2442
|
} = {}) {
|
|
1917
|
-
const detached = session.
|
|
2443
|
+
const detached = session.managedRepos.filter((repo) => repo.kind !== "fixture" && repo.detached).map((repo) => repo.name);
|
|
1918
2444
|
if (detached.length > 0) {
|
|
1919
|
-
workflowError(operation, "validation_failed", `Detached
|
|
2445
|
+
workflowError(operation, "validation_failed", `Detached managed repository heads detected: ${detached.join(", ")}.`, {
|
|
1920
2446
|
details: { detached }
|
|
1921
2447
|
});
|
|
1922
2448
|
}
|
|
1923
2449
|
if (requireCleanPackages) {
|
|
1924
|
-
const dirty = session.
|
|
2450
|
+
const dirty = session.managedRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
1925
2451
|
if (dirty.length > 0) {
|
|
1926
|
-
workflowError(operation, "validation_failed", `Dirty
|
|
2452
|
+
workflowError(operation, "validation_failed", `Dirty managed repos block ${operation}: ${dirty.join(", ")}.`, {
|
|
1927
2453
|
details: { dirty }
|
|
1928
2454
|
});
|
|
1929
2455
|
}
|
|
1930
2456
|
}
|
|
1931
2457
|
if (requireCurrentBranch && session.branchName) {
|
|
1932
|
-
const missing = session.
|
|
2458
|
+
const missing = session.managedRepos.filter((repo) => repo.kind !== "fixture").filter((repo) => repo.branchName !== session.branchName).map((repo) => ({ name: repo.name, branchName: repo.branchName }));
|
|
1933
2459
|
if (missing.length > 0) {
|
|
1934
|
-
workflowError(operation, "validation_failed", `
|
|
2460
|
+
workflowError(operation, "validation_failed", `Managed repository branch alignment is required for ${operation}.`, {
|
|
1935
2461
|
details: { expectedBranch: session.branchName, repos: missing }
|
|
1936
2462
|
});
|
|
1937
2463
|
}
|
|
1938
2464
|
}
|
|
1939
2465
|
const missingOriginRepos = [
|
|
1940
2466
|
session.rootRepo,
|
|
1941
|
-
...allowPackageReposWithoutOrigin ? [] : session.
|
|
2467
|
+
...allowPackageReposWithoutOrigin ? [] : session.managedRepos
|
|
1942
2468
|
].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
|
|
1943
2469
|
if (missingOriginRepos.length > 0 && operation !== "destroy") {
|
|
1944
2470
|
workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
|
|
@@ -1947,7 +2473,7 @@ function assertSessionBranchSafety(operation, session, {
|
|
|
1947
2473
|
}
|
|
1948
2474
|
}
|
|
1949
2475
|
function previewStateFor(tenantRoot, branchName) {
|
|
1950
|
-
const deployConfig =
|
|
2476
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
|
|
1951
2477
|
return loadDeployState(tenantRoot, deployConfig, {
|
|
1952
2478
|
target: createBranchPreviewDeployTarget(branchName)
|
|
1953
2479
|
});
|
|
@@ -1960,59 +2486,71 @@ function branchPreviewInitialized(tenantRoot, branchName) {
|
|
|
1960
2486
|
return false;
|
|
1961
2487
|
}
|
|
1962
2488
|
}
|
|
1963
|
-
async function
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
env:
|
|
1987
|
-
|
|
2489
|
+
async function reconcileWorkflowBranchPreview(tenantRoot, branchName, context, { initialize }) {
|
|
2490
|
+
return reconcileTreeseedBranchPreview({
|
|
2491
|
+
root: tenantRoot,
|
|
2492
|
+
branch: branchName,
|
|
2493
|
+
dryRun: false,
|
|
2494
|
+
execute: true,
|
|
2495
|
+
workflowRunId: context.workflow?.resumeRunId ?? void 0,
|
|
2496
|
+
initialize,
|
|
2497
|
+
env: context.env
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
async function reconcileTreeseedBranchPreview(input) {
|
|
2501
|
+
const target = { kind: "branch", branchName: input.branch };
|
|
2502
|
+
const graph = compileTreeseedDesiredResourceGraph({ tenantRoot: input.root, target });
|
|
2503
|
+
const selector = {
|
|
2504
|
+
environment: "staging",
|
|
2505
|
+
resourceKind: ["branch-preview"],
|
|
2506
|
+
...input.appId?.length ? { appId: input.appId } : {}
|
|
2507
|
+
};
|
|
2508
|
+
const units = compileTreeseedDesiredUnitsFromGraph(graph, selector);
|
|
2509
|
+
const reconcile = input.dryRun || !input.execute ? await planTreeseedReconciliation({ tenantRoot: input.root, target, env: input.env ?? process.env, units, selector }) : await reconcileTreeseedTarget({
|
|
2510
|
+
tenantRoot: input.root,
|
|
2511
|
+
target,
|
|
2512
|
+
env: input.env ?? process.env,
|
|
2513
|
+
units,
|
|
2514
|
+
selector,
|
|
2515
|
+
dryRun: false
|
|
1988
2516
|
});
|
|
1989
|
-
if ((deployResult.status ?? 1) !== 0) {
|
|
1990
|
-
workflowError("switch", "unsupported_state", "Preview deployment failed.", {
|
|
1991
|
-
exitCode: deployResult.status ?? 1,
|
|
1992
|
-
details: { branchName, wranglerPath }
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
const state = finalizeDeploymentState(tenantRoot, { target });
|
|
1996
2517
|
return {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2518
|
+
status: input.dryRun || !input.execute ? "planned" : "reconciled",
|
|
2519
|
+
branch: input.branch,
|
|
2520
|
+
initialize: input.initialize === true,
|
|
2521
|
+
reconcile
|
|
2001
2522
|
};
|
|
2002
2523
|
}
|
|
2003
|
-
function
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2524
|
+
async function destroyWorkflowBranchPreviewIfPresent(tenantRoot, branchName, context) {
|
|
2525
|
+
return destroyTreeseedBranchPreview({
|
|
2526
|
+
root: tenantRoot,
|
|
2527
|
+
branch: branchName,
|
|
2528
|
+
dryRun: false,
|
|
2529
|
+
execute: true,
|
|
2530
|
+
reason: "close",
|
|
2531
|
+
env: context?.env
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
async function destroyTreeseedBranchPreview(input) {
|
|
2535
|
+
const target = { kind: "branch", branchName: input.branch };
|
|
2536
|
+
const graph = compileTreeseedDesiredResourceGraph({ tenantRoot: input.root, target });
|
|
2537
|
+
const selector = {
|
|
2538
|
+
environment: "staging",
|
|
2539
|
+
resourceKind: ["branch-preview", "branch-preview-cleanup"]
|
|
2540
|
+
};
|
|
2541
|
+
const units = compileTreeseedDesiredUnitsFromGraph(graph, selector).map((unit) => unit.unitType === "branch-preview-cleanup" ? { ...unit, spec: { ...unit.spec, reason: input.reason } } : unit);
|
|
2542
|
+
const reconcile = input.dryRun || !input.execute ? await planTreeseedReconciliation({ tenantRoot: input.root, target, env: input.env ?? process.env, units, selector }) : await destroyTreeseedTargetUnits({
|
|
2543
|
+
tenantRoot: input.root,
|
|
2544
|
+
target,
|
|
2545
|
+
env: input.env ?? process.env,
|
|
2546
|
+
units,
|
|
2547
|
+
selector
|
|
2548
|
+
});
|
|
2013
2549
|
return {
|
|
2014
|
-
|
|
2015
|
-
|
|
2550
|
+
status: input.dryRun || !input.execute ? "planned" : "destroyed",
|
|
2551
|
+
branch: input.branch,
|
|
2552
|
+
reason: input.reason,
|
|
2553
|
+
reconcile
|
|
2016
2554
|
};
|
|
2017
2555
|
}
|
|
2018
2556
|
function resolveDestroyConfirmation(context, expected, input) {
|
|
@@ -2036,8 +2574,8 @@ function resolveDestroyConfirmation(context, expected, input) {
|
|
|
2036
2574
|
function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
2037
2575
|
try {
|
|
2038
2576
|
if (remoteBranchExists(repoDir, branch)) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2577
|
+
runGit(["pull", "--rebase", "origin", branch], { cwd: repoDir });
|
|
2578
|
+
runGit(["push", "origin", branch], { cwd: repoDir });
|
|
2041
2579
|
return {
|
|
2042
2580
|
remoteBranchExisted: true,
|
|
2043
2581
|
pulledRebase: true,
|
|
@@ -2046,7 +2584,7 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
|
2046
2584
|
conflicts: false
|
|
2047
2585
|
};
|
|
2048
2586
|
}
|
|
2049
|
-
|
|
2587
|
+
runGit(["push", "-u", "origin", branch], { cwd: repoDir });
|
|
2050
2588
|
return {
|
|
2051
2589
|
remoteBranchExisted: false,
|
|
2052
2590
|
pulledRebase: false,
|
|
@@ -2095,10 +2633,10 @@ function checkoutOrCreateSaveBranch(repoDir, branch) {
|
|
|
2095
2633
|
return branch;
|
|
2096
2634
|
}
|
|
2097
2635
|
if (remoteBranchExists(repoDir, branch)) {
|
|
2098
|
-
|
|
2636
|
+
runGit(["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
|
|
2099
2637
|
return branch;
|
|
2100
2638
|
}
|
|
2101
|
-
|
|
2639
|
+
runGit(["checkout", "-b", branch], { cwd: repoDir });
|
|
2102
2640
|
return branch;
|
|
2103
2641
|
}
|
|
2104
2642
|
function runPackageVerifyLocal(pkgDir) {
|
|
@@ -2108,8 +2646,8 @@ function branchNeedsSync(repoDir, branch) {
|
|
|
2108
2646
|
if (!remoteBranchExists(repoDir, branch)) {
|
|
2109
2647
|
return true;
|
|
2110
2648
|
}
|
|
2111
|
-
const localHead =
|
|
2112
|
-
const remoteHead =
|
|
2649
|
+
const localHead = runGit(["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
|
|
2650
|
+
const remoteHead = runGit(["rev-parse", `origin/${branch}`], { cwd: repoDir, capture: true }).trim();
|
|
2113
2651
|
return localHead !== remoteHead;
|
|
2114
2652
|
}
|
|
2115
2653
|
function savePackageRepo(report, message, branch, shouldVerify) {
|
|
@@ -2119,7 +2657,7 @@ function savePackageRepo(report, message, branch, shouldVerify) {
|
|
|
2119
2657
|
const needsSync = branchNeedsSync(report.path, branch);
|
|
2120
2658
|
if (!report.dirty && !needsSync) {
|
|
2121
2659
|
report.skippedReason = "clean";
|
|
2122
|
-
report.commitSha =
|
|
2660
|
+
report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
2123
2661
|
return report;
|
|
2124
2662
|
}
|
|
2125
2663
|
if (shouldVerify && report.dirty) {
|
|
@@ -2127,11 +2665,11 @@ function savePackageRepo(report, message, branch, shouldVerify) {
|
|
|
2127
2665
|
report.verified = true;
|
|
2128
2666
|
}
|
|
2129
2667
|
if (report.dirty) {
|
|
2130
|
-
|
|
2131
|
-
|
|
2668
|
+
runGit(["add", "-A"], { cwd: report.path });
|
|
2669
|
+
runGit(["commit", "-m", message], { cwd: report.path });
|
|
2132
2670
|
report.committed = true;
|
|
2133
2671
|
}
|
|
2134
|
-
report.commitSha =
|
|
2672
|
+
report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
2135
2673
|
const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
|
|
2136
2674
|
report.pushed = branchSync.pushed === true;
|
|
2137
2675
|
if (!report.dirty && needsSync) {
|
|
@@ -2169,14 +2707,14 @@ function ensureLocalTaskBranch(repoDir, branchName) {
|
|
|
2169
2707
|
}
|
|
2170
2708
|
return true;
|
|
2171
2709
|
}
|
|
2172
|
-
function cleanupTaskBranchReport(report, branchName,
|
|
2710
|
+
function cleanupTaskBranchReport(report, branchName, _message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
|
|
2173
2711
|
if (!ensureLocalTaskBranch(report.path, branchName)) {
|
|
2174
2712
|
report.skippedReason = "branch-missing";
|
|
2175
2713
|
return report;
|
|
2176
2714
|
}
|
|
2177
|
-
const
|
|
2178
|
-
report.tagName =
|
|
2179
|
-
report.commitSha =
|
|
2715
|
+
const tag = createDeprecatedTaskTag(report.path, branchName, _message);
|
|
2716
|
+
report.tagName = tag.tagName;
|
|
2717
|
+
report.commitSha = updateHead(report.path);
|
|
2180
2718
|
report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
|
|
2181
2719
|
syncBranchWithOrigin(report.path, targetBranch);
|
|
2182
2720
|
if (deleteBranch) {
|
|
@@ -2240,7 +2778,7 @@ function collectReleasePackageSelection(root) {
|
|
|
2240
2778
|
};
|
|
2241
2779
|
}
|
|
2242
2780
|
function hasStagedChanges(repoDir) {
|
|
2243
|
-
return
|
|
2781
|
+
return runGit(["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
|
|
2244
2782
|
}
|
|
2245
2783
|
async function workflowStatus(helpers, input = {}) {
|
|
2246
2784
|
return withContextEnv(helpers.context.env, async () => {
|
|
@@ -2554,6 +3092,7 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
2554
3092
|
sync,
|
|
2555
3093
|
env: helpers.context.env,
|
|
2556
3094
|
checkConnections: bootstrapOnly || sync !== "none" || scopes.some((scope) => scope !== "local"),
|
|
3095
|
+
initializePersistent: bootstrapOnly,
|
|
2557
3096
|
systems: bootstrapSystemsInput,
|
|
2558
3097
|
skipUnavailable,
|
|
2559
3098
|
bootstrapExecution,
|
|
@@ -2661,7 +3200,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
2661
3200
|
const mode = session.mode;
|
|
2662
3201
|
const repoDir = session.gitRoot;
|
|
2663
3202
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
2664
|
-
const packageReports =
|
|
3203
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
2665
3204
|
let previewResult = null;
|
|
2666
3205
|
const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
2667
3206
|
if (executionMode === "plan") {
|
|
@@ -2671,6 +3210,14 @@ async function workflowSwitch(helpers, input) {
|
|
|
2671
3210
|
report.created = !local && !remote;
|
|
2672
3211
|
report.resumed = local || remote;
|
|
2673
3212
|
}
|
|
3213
|
+
const previewPlan = preview ? await reconcileTreeseedBranchPreview({
|
|
3214
|
+
root,
|
|
3215
|
+
branch: branchName,
|
|
3216
|
+
dryRun: true,
|
|
3217
|
+
execute: false,
|
|
3218
|
+
initialize: !branchPreviewInitialized(root, branchName),
|
|
3219
|
+
env: helpers.context.env
|
|
3220
|
+
}) : null;
|
|
2674
3221
|
return buildWorkflowResult(
|
|
2675
3222
|
"switch",
|
|
2676
3223
|
root,
|
|
@@ -2688,7 +3235,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
2688
3235
|
...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
|
|
2689
3236
|
{ id: "workspace-link", description: "Apply local workspace links for integrated development" },
|
|
2690
3237
|
...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
|
|
2691
|
-
]
|
|
3238
|
+
],
|
|
3239
|
+
previewPlan
|
|
2692
3240
|
},
|
|
2693
3241
|
{
|
|
2694
3242
|
executionMode,
|
|
@@ -2738,8 +3286,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
2738
3286
|
rootRepo.resumed = rootSwitch.resumed;
|
|
2739
3287
|
rootRepo.commitSha = headCommit(repoDir);
|
|
2740
3288
|
rootRepo.pushed = rootSwitch.created;
|
|
2741
|
-
for (const
|
|
2742
|
-
const report = findReportByName(packageReports,
|
|
3289
|
+
for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
|
|
3290
|
+
const report = findReportByName(packageReports, managedRepo.name);
|
|
2743
3291
|
if (!report) {
|
|
2744
3292
|
continue;
|
|
2745
3293
|
}
|
|
@@ -2747,16 +3295,16 @@ async function workflowSwitch(helpers, input) {
|
|
|
2747
3295
|
root,
|
|
2748
3296
|
workflowRun.runId,
|
|
2749
3297
|
`switch-${report.name}`,
|
|
2750
|
-
() => checkoutTaskBranchFromStaging(
|
|
3298
|
+
() => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
|
|
2751
3299
|
createIfMissing: input.createIfMissing !== false,
|
|
2752
3300
|
pushIfCreated: false
|
|
2753
3301
|
})
|
|
2754
3302
|
);
|
|
2755
|
-
report.branch = currentBranch(
|
|
3303
|
+
report.branch = currentBranch(managedRepo.dir) || branchName;
|
|
2756
3304
|
report.created = packageSwitch.created;
|
|
2757
3305
|
report.resumed = packageSwitch.resumed;
|
|
2758
|
-
report.commitSha = headCommit(
|
|
2759
|
-
report.dirty = hasMeaningfulChanges(
|
|
3306
|
+
report.commitSha = headCommit(managedRepo.dir);
|
|
3307
|
+
report.dirty = hasMeaningfulChanges(managedRepo.dir);
|
|
2760
3308
|
}
|
|
2761
3309
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
|
|
2762
3310
|
const stateAfterSwitch = resolveTreeseedWorkflowState(root);
|
|
@@ -2765,7 +3313,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
2765
3313
|
root,
|
|
2766
3314
|
workflowRun.runId,
|
|
2767
3315
|
"preview",
|
|
2768
|
-
() =>
|
|
3316
|
+
() => reconcileWorkflowBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
|
|
2769
3317
|
) ?? null;
|
|
2770
3318
|
}
|
|
2771
3319
|
const state = resolveTreeseedWorkflowState(root);
|
|
@@ -2819,17 +3367,386 @@ async function workflowSwitch(helpers, input) {
|
|
|
2819
3367
|
toError("switch", error);
|
|
2820
3368
|
}
|
|
2821
3369
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3370
|
+
function normalizeUpdateStrategy(strategy) {
|
|
3371
|
+
return strategy === "ff-only" ? "ff-only" : "merge";
|
|
3372
|
+
}
|
|
3373
|
+
function normalizeUpdateSource(source) {
|
|
3374
|
+
const normalized = String(source ?? STAGING_BRANCH).trim();
|
|
3375
|
+
return normalized || STAGING_BRANCH;
|
|
3376
|
+
}
|
|
3377
|
+
function gitOutput(args, cwd, allowFailure = false) {
|
|
3378
|
+
return runTreeseedGit(args, {
|
|
3379
|
+
cwd,
|
|
3380
|
+
mode: classifyTreeseedGitMode(args),
|
|
3381
|
+
allowFailure
|
|
3382
|
+
}).stdout.trim();
|
|
3383
|
+
}
|
|
3384
|
+
function updateHead(repoDir) {
|
|
3385
|
+
return gitOutput(["rev-parse", "HEAD"], repoDir, true) || null;
|
|
3386
|
+
}
|
|
3387
|
+
function updateStatusLines(repoDir) {
|
|
3388
|
+
const output = gitOutput(["status", "--porcelain"], repoDir, true);
|
|
3389
|
+
return output ? output.split(/\r?\n/u).filter(Boolean) : [];
|
|
3390
|
+
}
|
|
3391
|
+
function updateChangedFiles(repoDir) {
|
|
3392
|
+
return updateStatusLines(repoDir).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
3393
|
+
}
|
|
3394
|
+
function updateConflictedFiles(repoDir) {
|
|
3395
|
+
return updateStatusLines(repoDir).filter((line) => {
|
|
3396
|
+
const status = line.slice(0, 2);
|
|
3397
|
+
return status.includes("U") || ["AA", "DD"].includes(status);
|
|
3398
|
+
}).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
3399
|
+
}
|
|
3400
|
+
function sourceBranchExists(repoDir, sourceBranch) {
|
|
3401
|
+
return runTreeseedGit(["ls-remote", "--exit-code", "--heads", "origin", sourceBranch], {
|
|
3402
|
+
cwd: repoDir,
|
|
3403
|
+
mode: "read",
|
|
3404
|
+
allowFailure: true
|
|
3405
|
+
}).status === 0;
|
|
3406
|
+
}
|
|
3407
|
+
function localRemoteRefExists(repoDir, sourceBranch) {
|
|
3408
|
+
return runTreeseedGitOk(["show-ref", "--verify", `refs/remotes/origin/${sourceBranch}`], {
|
|
3409
|
+
cwd: repoDir,
|
|
3410
|
+
mode: "read"
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
function updateAheadBehind(repoDir, branch, sourceRef) {
|
|
3414
|
+
if (!localRemoteRefExists(repoDir, sourceRef.replace(/^origin\//u, ""))) {
|
|
3415
|
+
return { ahead: null, behind: null };
|
|
3416
|
+
}
|
|
3417
|
+
const output = gitOutput(["rev-list", "--left-right", "--count", `${branch}...${sourceRef}`], repoDir, true);
|
|
3418
|
+
const [aheadRaw, behindRaw] = output.split(/\s+/u);
|
|
3419
|
+
const ahead = Number.parseInt(aheadRaw ?? "", 10);
|
|
3420
|
+
const behind = Number.parseInt(behindRaw ?? "", 10);
|
|
3421
|
+
return {
|
|
3422
|
+
ahead: Number.isFinite(ahead) ? ahead : null,
|
|
3423
|
+
behind: Number.isFinite(behind) ? behind : null
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
function updatePlanChangedFiles(repoDir, sourceRef) {
|
|
3427
|
+
if (!runTreeseedGitOk(["show-ref", "--verify", `refs/remotes/${sourceRef}`], { cwd: repoDir, mode: "read" })) {
|
|
3428
|
+
return [];
|
|
3429
|
+
}
|
|
3430
|
+
const output = gitOutput(["diff", "--name-only", `HEAD...${sourceRef}`], repoDir, true);
|
|
3431
|
+
return output ? output.split(/\r?\n/u).filter(Boolean).slice(0, 50) : [];
|
|
3432
|
+
}
|
|
3433
|
+
function planUpdateRepo(name, repoDir, branch, sourceBranch, strategy) {
|
|
3434
|
+
const sourceRef = `origin/${sourceBranch}`;
|
|
3435
|
+
const blockers = [];
|
|
3436
|
+
if (!sourceBranchExists(repoDir, sourceBranch)) {
|
|
3437
|
+
blockers.push(`origin/${sourceBranch} does not exist`);
|
|
3438
|
+
}
|
|
3439
|
+
const { ahead, behind } = blockers.length === 0 ? updateAheadBehind(repoDir, branch, sourceRef) : { ahead: null, behind: null };
|
|
3440
|
+
const status = blockers.length > 0 ? "blocked" : behind === 0 ? "up-to-date" : strategy === "ff-only" && ahead === 0 ? "fast-forward" : "merge-needed";
|
|
3441
|
+
return {
|
|
3442
|
+
name,
|
|
3443
|
+
path: repoDir,
|
|
3444
|
+
branch,
|
|
3445
|
+
sourceRef,
|
|
3446
|
+
action: blockers.length > 0 ? "blocked" : "planned",
|
|
3447
|
+
beforeHead: updateHead(repoDir),
|
|
3448
|
+
afterHead: null,
|
|
3449
|
+
pushed: false,
|
|
3450
|
+
changedFiles: updatePlanChangedFiles(repoDir, sourceRef),
|
|
3451
|
+
blockers,
|
|
3452
|
+
ahead,
|
|
3453
|
+
behind,
|
|
3454
|
+
status
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
function ensureUpdateRepoReady(operation, repo, expectedBranch) {
|
|
3458
|
+
if (repo.detached || !repo.branchName) {
|
|
3459
|
+
workflowError(operation, "validation_failed", `${repo.name} is detached; update requires attached branches.`, {
|
|
3460
|
+
details: { repo }
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
if (expectedBranch && repo.branchName !== expectedBranch) {
|
|
3464
|
+
workflowError(operation, "validation_failed", `${repo.name} is on ${repo.branchName}, expected ${expectedBranch}.`, {
|
|
3465
|
+
details: { repo, expectedBranch }
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
if (repo.dirty) {
|
|
3469
|
+
workflowError(operation, "validation_failed", `${repo.name} has local changes. Run \`npx trsd save --json "checkpoint before update"\` first.`, {
|
|
3470
|
+
details: { repo }
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
if (!repo.hasOriginRemote) {
|
|
3474
|
+
workflowError(operation, "validation_failed", `${repo.name} is missing an origin remote.`, {
|
|
3475
|
+
details: { repo }
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
function formatUpdateConflict(repoName, repoDir, sourceBranch, targetBranch) {
|
|
3480
|
+
const files = updateConflictedFiles(repoDir);
|
|
3481
|
+
const status = updateStatusLines(repoDir);
|
|
3482
|
+
return {
|
|
3483
|
+
message: [
|
|
3484
|
+
`Treeseed update hit a merge conflict in ${repoName}.`,
|
|
3485
|
+
`Repository: ${repoDir}`,
|
|
3486
|
+
`Target branch: ${targetBranch}`,
|
|
3487
|
+
`Source branch: origin/${sourceBranch}`,
|
|
3488
|
+
files.length > 0 ? `Conflicted files:
|
|
3489
|
+
${files.map((file) => `- ${file}`).join("\n")}` : "Conflicted files: inspect git status.",
|
|
3490
|
+
'Resolve the conflicts in that repository, then run `npx trsd save --json "resolve update conflict"` or abort manually and rerun `npx trsd update --from staging --json`.'
|
|
3491
|
+
].join("\n"),
|
|
3492
|
+
files,
|
|
3493
|
+
status
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
function mergeUpdateRepo(input) {
|
|
3497
|
+
const sourceRef = `origin/${input.sourceBranch}`;
|
|
3498
|
+
const beforeHead = updateHead(input.repoDir);
|
|
3499
|
+
runTreeseedGit(["fetch", "origin"], { cwd: input.repoDir, mode: "mutate" });
|
|
3500
|
+
if (!sourceBranchExists(input.repoDir, input.sourceBranch)) {
|
|
3501
|
+
return {
|
|
3502
|
+
name: input.name,
|
|
3503
|
+
path: input.repoDir,
|
|
3504
|
+
branch: input.branch,
|
|
3505
|
+
sourceRef,
|
|
3506
|
+
action: "blocked",
|
|
3507
|
+
beforeHead,
|
|
3508
|
+
afterHead: beforeHead,
|
|
3509
|
+
pushed: false,
|
|
3510
|
+
changedFiles: [],
|
|
3511
|
+
blockers: [`origin/${input.sourceBranch} does not exist`]
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
const mergeArgs = input.strategy === "ff-only" ? ["merge", "--ff-only", sourceRef] : ["merge", "--no-edit", sourceRef];
|
|
3515
|
+
const merge = runTreeseedGit(mergeArgs, {
|
|
3516
|
+
cwd: input.repoDir,
|
|
3517
|
+
mode: "mutate",
|
|
3518
|
+
allowFailure: true
|
|
3519
|
+
});
|
|
3520
|
+
if (merge.status !== 0) {
|
|
3521
|
+
const conflict = formatUpdateConflict(input.name, input.repoDir, input.sourceBranch, input.branch);
|
|
3522
|
+
throw new TreeseedWorkflowError("update", "merge_conflict", conflict.message, {
|
|
3523
|
+
details: {
|
|
3524
|
+
repo: input.name,
|
|
3525
|
+
path: input.repoDir,
|
|
3526
|
+
files: conflict.files,
|
|
3527
|
+
status: conflict.status,
|
|
3528
|
+
sourceBranch: input.sourceBranch,
|
|
3529
|
+
targetBranch: input.branch
|
|
3530
|
+
},
|
|
3531
|
+
exitCode: 12
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
const afterHead = updateHead(input.repoDir);
|
|
3535
|
+
const changed = beforeHead !== afterHead;
|
|
3536
|
+
let pushed = false;
|
|
3537
|
+
if (changed && input.push) {
|
|
3538
|
+
runTreeseedGit(["push", "origin", input.branch], { cwd: input.repoDir, mode: "mutate" });
|
|
3539
|
+
pushed = true;
|
|
3540
|
+
}
|
|
3541
|
+
return {
|
|
3542
|
+
name: input.name,
|
|
3543
|
+
path: input.repoDir,
|
|
3544
|
+
branch: input.branch,
|
|
3545
|
+
sourceRef,
|
|
3546
|
+
action: changed ? input.strategy === "ff-only" ? "fast-forwarded" : "merged" : "up-to-date",
|
|
3547
|
+
beforeHead,
|
|
3548
|
+
afterHead,
|
|
3549
|
+
pushed,
|
|
3550
|
+
changedFiles: [],
|
|
3551
|
+
blockers: []
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
function commitRootUpdateIfNeeded(root, branch, push) {
|
|
3555
|
+
const changedFiles = updateChangedFiles(repoRoot(root));
|
|
3556
|
+
if (changedFiles.length === 0) {
|
|
3557
|
+
let pushed2 = false;
|
|
3558
|
+
if (push) {
|
|
3559
|
+
runTreeseedGit(["push", "origin", branch], { cwd: repoRoot(root), mode: "mutate" });
|
|
3560
|
+
pushed2 = true;
|
|
3561
|
+
}
|
|
3562
|
+
return {
|
|
3563
|
+
committed: false,
|
|
3564
|
+
pushed: pushed2,
|
|
3565
|
+
commitSha: updateHead(repoRoot(root)),
|
|
3566
|
+
changedFiles
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
runTreeseedGit(["add", "-A"], { cwd: repoRoot(root), mode: "mutate" });
|
|
3570
|
+
runTreeseedGit(["commit", "-m", `chore(workflow): update ${branch} from staging`], { cwd: repoRoot(root), mode: "mutate" });
|
|
3571
|
+
const commitSha = updateHead(repoRoot(root));
|
|
3572
|
+
let pushed = false;
|
|
3573
|
+
if (push) {
|
|
3574
|
+
runTreeseedGit(["push", "origin", branch], { cwd: repoRoot(root), mode: "mutate" });
|
|
3575
|
+
pushed = true;
|
|
3576
|
+
}
|
|
3577
|
+
return {
|
|
3578
|
+
committed: true,
|
|
3579
|
+
pushed,
|
|
3580
|
+
commitSha,
|
|
3581
|
+
changedFiles
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
async function workflowUpdate(helpers, input = {}) {
|
|
3585
|
+
try {
|
|
3586
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
3587
|
+
const tenantRoot = resolveProjectRootOrThrow("update", helpers.cwd());
|
|
3588
|
+
const root = workspaceRoot(tenantRoot);
|
|
3589
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3590
|
+
const sourceBranch = normalizeUpdateSource(input.from);
|
|
3591
|
+
const strategy = normalizeUpdateStrategy(input.strategy);
|
|
3592
|
+
const push = input.push !== false;
|
|
3593
|
+
const executionMode = normalizeExecutionMode(input);
|
|
3594
|
+
const branch = session.branchName;
|
|
3595
|
+
if (!branch) {
|
|
3596
|
+
workflowError("update", "validation_failed", "Treeseed update requires an attached current branch.");
|
|
3597
|
+
}
|
|
3598
|
+
if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
|
|
3599
|
+
workflowError("update", "validation_failed", `Treeseed update must run from a task branch, not ${branch}.`, {
|
|
3600
|
+
details: { branch }
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
if (sourceBranch === branch) {
|
|
3604
|
+
workflowError("update", "validation_failed", "Treeseed update source branch cannot match the current branch.", {
|
|
3605
|
+
details: { branch, sourceBranch }
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
ensureUpdateRepoReady("update", session.rootRepo);
|
|
3609
|
+
for (const repo of session.managedRepos) {
|
|
3610
|
+
ensureUpdateRepoReady("update", repo, branch);
|
|
3611
|
+
}
|
|
3612
|
+
const repoPlans = session.managedRepos.map((repo) => planUpdateRepo(repo.name, repo.path, branch, sourceBranch, strategy));
|
|
3613
|
+
const rootPlan = planUpdateRepo("@treeseed/market", session.gitRoot, branch, sourceBranch, strategy);
|
|
3614
|
+
const blockers = [...repoPlans, rootPlan].flatMap((repo) => repo.blockers.map((blocker) => `${repo.name}: ${blocker}`));
|
|
3615
|
+
if (executionMode === "plan") {
|
|
3616
|
+
return buildWorkflowResult("update", root, {
|
|
3617
|
+
mode: session.mode,
|
|
3618
|
+
branch,
|
|
3619
|
+
sourceBranch,
|
|
3620
|
+
sourceRef: `origin/${sourceBranch}`,
|
|
3621
|
+
strategy,
|
|
3622
|
+
pushed: false,
|
|
3623
|
+
plan: true,
|
|
3624
|
+
repos: repoPlans,
|
|
3625
|
+
rootRepo: rootPlan,
|
|
3626
|
+
conflicts: [],
|
|
3627
|
+
blockers,
|
|
3628
|
+
...worktreePayload(root, input.worktreeMode)
|
|
3629
|
+
}, {
|
|
3630
|
+
executionMode,
|
|
3631
|
+
includeFinalState: false,
|
|
3632
|
+
nextSteps: createNextSteps([
|
|
3633
|
+
{ operation: "update", reason: "Run without --plan to merge staging into the current branch.", input: { from: sourceBranch } }
|
|
3634
|
+
])
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
if (blockers.length > 0) {
|
|
3638
|
+
workflowError("update", "validation_failed", `Treeseed update is blocked:
|
|
3639
|
+
${blockers.join("\n")}`, {
|
|
3640
|
+
details: { blockers, repos: repoPlans, rootRepo: rootPlan }
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
const workflowRun = acquireWorkflowRun(
|
|
3644
|
+
"update",
|
|
3645
|
+
session,
|
|
3646
|
+
{ from: sourceBranch, strategy, push, workspaceLinks: input.workspaceLinks ?? "auto" },
|
|
3647
|
+
[
|
|
3648
|
+
{ id: "validate-update", description: `Validate update from ${sourceBranch}`, repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
|
|
3649
|
+
...session.managedRepos.map((repo) => ({
|
|
3650
|
+
id: `update-${repo.name}`,
|
|
3651
|
+
description: `Merge origin/${sourceBranch} into ${repo.name}`,
|
|
3652
|
+
repoName: repo.name,
|
|
3653
|
+
repoPath: repo.path,
|
|
3654
|
+
branch,
|
|
3655
|
+
resumable: true
|
|
3656
|
+
})),
|
|
3657
|
+
{ id: "update-root", description: `Merge origin/${sourceBranch} into market`, repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
|
|
3658
|
+
{ id: "refresh-root-pointers", description: "Commit updated root pointers if package heads changed", repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
|
|
3659
|
+
{ id: "restore-workspace-links", description: "Restore local workspace links", repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true }
|
|
3660
|
+
],
|
|
3661
|
+
helpers.context
|
|
3662
|
+
);
|
|
3663
|
+
try {
|
|
3664
|
+
await executeJournalStep(root, workflowRun.runId, "validate-update", () => ({
|
|
3665
|
+
branch,
|
|
3666
|
+
sourceBranch,
|
|
3667
|
+
strategy,
|
|
3668
|
+
push
|
|
3669
|
+
}));
|
|
3670
|
+
const repos = [];
|
|
3671
|
+
for (const repo of session.managedRepos) {
|
|
3672
|
+
const result = await executeJournalStep(root, workflowRun.runId, `update-${repo.name}`, () => mergeUpdateRepo({
|
|
3673
|
+
name: repo.name,
|
|
3674
|
+
repoDir: repo.path,
|
|
3675
|
+
branch,
|
|
3676
|
+
sourceBranch,
|
|
3677
|
+
strategy,
|
|
3678
|
+
push
|
|
3679
|
+
}));
|
|
3680
|
+
if (result) repos.push(result);
|
|
3681
|
+
}
|
|
3682
|
+
const rootMerge = await executeJournalStep(root, workflowRun.runId, "update-root", () => mergeUpdateRepo({
|
|
3683
|
+
name: "@treeseed/market",
|
|
3684
|
+
repoDir: session.gitRoot,
|
|
3685
|
+
branch,
|
|
3686
|
+
sourceBranch,
|
|
3687
|
+
strategy,
|
|
3688
|
+
push: false
|
|
3689
|
+
}));
|
|
3690
|
+
const rootCommit = await executeJournalStep(root, workflowRun.runId, "refresh-root-pointers", () => commitRootUpdateIfNeeded(root, branch, push));
|
|
3691
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "restore-workspace-links", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
|
|
3692
|
+
const rootRepo = {
|
|
3693
|
+
...rootMerge,
|
|
3694
|
+
action: rootCommit?.committed ? "committed" : rootMerge.action,
|
|
3695
|
+
commitSha: rootCommit?.commitSha ?? rootMerge.afterHead,
|
|
3696
|
+
pushed: rootCommit?.pushed ?? false,
|
|
3697
|
+
changedFiles: rootCommit?.changedFiles ?? rootMerge.changedFiles
|
|
3698
|
+
};
|
|
3699
|
+
const payload = {
|
|
3700
|
+
mode: session.mode,
|
|
3701
|
+
branch,
|
|
3702
|
+
sourceBranch,
|
|
3703
|
+
sourceRef: `origin/${sourceBranch}`,
|
|
3704
|
+
strategy,
|
|
3705
|
+
pushed: push,
|
|
3706
|
+
plan: false,
|
|
3707
|
+
repos,
|
|
3708
|
+
rootRepo,
|
|
3709
|
+
conflicts: [],
|
|
3710
|
+
workspaceLinks,
|
|
3711
|
+
...worktreePayload(root, input.worktreeMode)
|
|
3712
|
+
};
|
|
3713
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
3714
|
+
return buildWorkflowResult("update", root, payload, {
|
|
3715
|
+
runId: workflowRun.runId,
|
|
3716
|
+
summary: `Treeseed update merged ${sourceBranch} into ${branch}.`,
|
|
3717
|
+
includeFinalState: false,
|
|
3718
|
+
nextSteps: createNextSteps([
|
|
3719
|
+
{ operation: "save", reason: "Checkpoint any follow-up conflict resolutions or generated pointer changes.", input: { message: "sync with staging" } },
|
|
3720
|
+
{ operation: "stage", reason: "Merge the updated task branch into staging when it is ready.", input: { message: "describe the resolution" } }
|
|
3721
|
+
])
|
|
3722
|
+
});
|
|
3723
|
+
} catch (error) {
|
|
3724
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
3725
|
+
resumable: true,
|
|
3726
|
+
runId: workflowRun.runId,
|
|
3727
|
+
command: "update",
|
|
3728
|
+
message: `Resume the interrupted update for ${branch}.`,
|
|
3729
|
+
recoverCommand: "treeseed recover",
|
|
3730
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
3731
|
+
});
|
|
3732
|
+
throw error;
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
toError("update", error);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
async function workflowDev(helpers, input = {}) {
|
|
3740
|
+
try {
|
|
3741
|
+
return withContextEnv(helpers.context.env, async () => {
|
|
3742
|
+
if (helpers.context.transport === "api") {
|
|
3743
|
+
workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
|
|
3744
|
+
}
|
|
3745
|
+
const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
|
|
3746
|
+
const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
|
|
3747
|
+
const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
|
|
3748
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
|
|
3749
|
+
assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
|
|
2833
3750
|
const args = [packageScriptPath("tenant-dev")];
|
|
2834
3751
|
if (input.watch) {
|
|
2835
3752
|
args.push("--watch");
|
|
@@ -2897,6 +3814,7 @@ async function workflowSave(helpers, input) {
|
|
|
2897
3814
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2898
3815
|
const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
|
|
2899
3816
|
const root = workspaceRoot(tenantRoot);
|
|
3817
|
+
ensureTreeseedLocalStateExcluded(root);
|
|
2900
3818
|
const rootBranch = currentBranch(repoRoot(root)) || null;
|
|
2901
3819
|
reattachRepairablePackageRepos(root, [rootBranch, STAGING_BRANCH, PRODUCTION_BRANCH].filter((branch2) => Boolean(branch2)), {
|
|
2902
3820
|
operation: "save",
|
|
@@ -2916,6 +3834,9 @@ async function workflowSave(helpers, input) {
|
|
|
2916
3834
|
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableSaveRun(root, branch) : null;
|
|
2917
3835
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2918
3836
|
const message = String(effectiveInput.message ?? "").trim();
|
|
3837
|
+
const saveLane = normalizeSaveLane(effectiveInput.lane);
|
|
3838
|
+
const saveCiMode = normalizeSaveCiMode(effectiveInput.ciMode, branch, saveLane);
|
|
3839
|
+
const releaseCandidateMode = normalizeReleaseCandidateMode(effectiveInput.releaseCandidate, "save", saveLane);
|
|
2919
3840
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
2920
3841
|
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
2921
3842
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
@@ -2925,7 +3846,7 @@ async function workflowSave(helpers, input) {
|
|
|
2925
3846
|
if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
|
|
2926
3847
|
workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
|
|
2927
3848
|
}
|
|
2928
|
-
const packageReports =
|
|
3849
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
2929
3850
|
const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
|
|
2930
3851
|
const blockers = [];
|
|
2931
3852
|
if (executionMode === "plan") {
|
|
@@ -2942,12 +3863,13 @@ async function workflowSave(helpers, input) {
|
|
|
2942
3863
|
message,
|
|
2943
3864
|
bump: effectiveInput.bump ?? "patch",
|
|
2944
3865
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
2945
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
3866
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
2946
3867
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2947
3868
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2948
3869
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2949
3870
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
|
|
2950
3871
|
});
|
|
3872
|
+
const applicationSelection = selectWorkflowApplications(root);
|
|
2951
3873
|
const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
|
|
2952
3874
|
return buildWorkflowResult(
|
|
2953
3875
|
"save",
|
|
@@ -2967,8 +3889,11 @@ async function workflowSave(helpers, input) {
|
|
|
2967
3889
|
failure: planAutoResumeRun.failure
|
|
2968
3890
|
} : null,
|
|
2969
3891
|
workspaceLinks,
|
|
2970
|
-
ciMode:
|
|
3892
|
+
ciMode: saveCiMode,
|
|
3893
|
+
lane: saveLane,
|
|
2971
3894
|
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
3895
|
+
releaseCandidateMode,
|
|
3896
|
+
applicationSelection,
|
|
2972
3897
|
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2973
3898
|
repositoryPlan,
|
|
2974
3899
|
waves: repositoryPlan.waves,
|
|
@@ -2977,8 +3902,9 @@ async function workflowSave(helpers, input) {
|
|
|
2977
3902
|
{ id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
|
|
2978
3903
|
...repositoryPlan.plannedSteps,
|
|
2979
3904
|
{ id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
|
|
2980
|
-
...shouldUseHostedSaveCi(effectiveInput, branch) ? [{ id: "hosted-ci", description: `Wait for hosted save workflows on ${branch}` }] : [],
|
|
2981
|
-
...
|
|
3905
|
+
...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{ id: "hosted-ci", description: saveHostedEnvironmentForBranch(branch) ? `Reconcile and verify hosted deployments for ${saveHostedEnvironmentForBranch(branch)}` : `Wait for hosted save workflows on ${branch}` }] : [],
|
|
3906
|
+
...saveLane === "promotion" ? [{ id: "release-proof", description: "Run or reuse authoritative hosted release proof records for exact package refs" }] : [],
|
|
3907
|
+
...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{ id: "release-candidate", description: `Run ${releaseCandidateMode} release-candidate readiness checks for the saved staging state` }] : [],
|
|
2982
3908
|
{ id: "workspace-link", description: "Restore local workspace links after save" },
|
|
2983
3909
|
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
2984
3910
|
]
|
|
@@ -3010,14 +3936,16 @@ async function workflowSave(helpers, input) {
|
|
|
3010
3936
|
verify: effectiveInput.verify !== false,
|
|
3011
3937
|
bump: effectiveInput.bump ?? "patch",
|
|
3012
3938
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
3013
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
3939
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
3014
3940
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3015
3941
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3016
3942
|
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
|
|
3017
|
-
ciMode:
|
|
3943
|
+
ciMode: saveCiMode,
|
|
3944
|
+
lane: saveLane,
|
|
3018
3945
|
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3019
3946
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
3020
3947
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto",
|
|
3948
|
+
releaseCandidate: releaseCandidateMode,
|
|
3021
3949
|
verifyDeployedResources: effectiveInput.verifyDeployedResources === true
|
|
3022
3950
|
},
|
|
3023
3951
|
[
|
|
@@ -3029,15 +3957,23 @@ async function workflowSave(helpers, input) {
|
|
|
3029
3957
|
branch,
|
|
3030
3958
|
resumable: true
|
|
3031
3959
|
},
|
|
3032
|
-
...shouldUseHostedSaveCi(effectiveInput, branch) ? [{
|
|
3960
|
+
...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{
|
|
3033
3961
|
id: "hosted-ci",
|
|
3034
|
-
description: `Wait for hosted save workflows on ${branch}`,
|
|
3962
|
+
description: saveHostedEnvironmentForBranch(branch) ? `Reconcile and verify hosted deployments for ${saveHostedEnvironmentForBranch(branch)}` : `Wait for hosted save workflows on ${branch}`,
|
|
3035
3963
|
repoName: rootRepo.name,
|
|
3036
3964
|
repoPath: rootRepo.path,
|
|
3037
3965
|
branch,
|
|
3038
3966
|
resumable: true
|
|
3039
3967
|
}] : [],
|
|
3040
|
-
...
|
|
3968
|
+
...saveLane === "promotion" ? [{
|
|
3969
|
+
id: "release-proof",
|
|
3970
|
+
description: "Run authoritative hosted release proof records",
|
|
3971
|
+
repoName: rootRepo.name,
|
|
3972
|
+
repoPath: rootRepo.path,
|
|
3973
|
+
branch,
|
|
3974
|
+
resumable: true
|
|
3975
|
+
}] : [],
|
|
3976
|
+
...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{
|
|
3041
3977
|
id: "release-candidate",
|
|
3042
3978
|
description: "Run release-candidate readiness checks",
|
|
3043
3979
|
repoName: rootRepo.name,
|
|
@@ -3078,27 +4014,29 @@ async function workflowSave(helpers, input) {
|
|
|
3078
4014
|
message,
|
|
3079
4015
|
bump: effectiveInput.bump ?? "patch",
|
|
3080
4016
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
3081
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
4017
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
3082
4018
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3083
4019
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3084
4020
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
3085
4021
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
3086
4022
|
workflowRunId: workflowRun.runId,
|
|
4023
|
+
deferPushUntilVerified: branch === STAGING_BRANCH,
|
|
3087
4024
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
3088
|
-
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
3089
|
-
const
|
|
3090
|
-
const rootReportForWave = nodes.some((node) => node.
|
|
4025
|
+
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
4026
|
+
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
4027
|
+
const rootReportForWave = nodes.some((node) => node.id === ".") ? waveRootRepo : null;
|
|
4028
|
+
const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
|
|
3091
4029
|
const gates = [
|
|
3092
|
-
...
|
|
3093
|
-
...rootReportForWave ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
|
|
4030
|
+
...gatesForSavedRepositoryReports(root, nonRootReportsForWave),
|
|
4031
|
+
...rootReportForWave && !hostedEnvironment ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
|
|
3094
4032
|
];
|
|
3095
4033
|
if (gates.length === 0) {
|
|
3096
4034
|
return [];
|
|
3097
4035
|
}
|
|
3098
|
-
const
|
|
3099
|
-
if (
|
|
3100
|
-
helpers.write(`[save][workflow] Waiting for hosted
|
|
3101
|
-
} else if (rootReportForWave) {
|
|
4036
|
+
const repositoryNames = gates.map((gate) => gate.name).join(", ");
|
|
4037
|
+
if (nonRootReportsForWave.length > 0) {
|
|
4038
|
+
helpers.write(`[save][workflow] Waiting for hosted repository gates before saving dependents: ${repositoryNames}.`);
|
|
4039
|
+
} else if (rootReportForWave && !hostedEnvironment) {
|
|
3102
4040
|
helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
|
|
3103
4041
|
}
|
|
3104
4042
|
return waitForWorkflowGates("save", gates, "hosted", {
|
|
@@ -3115,7 +4053,7 @@ async function workflowSave(helpers, input) {
|
|
|
3115
4053
|
const savedPackageReports = saveResult?.repos ?? packageReports;
|
|
3116
4054
|
const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
|
|
3117
4055
|
helpers.write("[save][workflow] Repository save phase complete; checking command readiness.");
|
|
3118
|
-
const head = savedRootRepo.commitSha ??
|
|
4056
|
+
const head = savedRootRepo.commitSha ?? runGit(["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
3119
4057
|
const commitCreated = savedRootRepo.committed === true;
|
|
3120
4058
|
const branchSync = {
|
|
3121
4059
|
...savedRootRepo.publishWait ?? {},
|
|
@@ -3131,43 +4069,13 @@ async function workflowSave(helpers, input) {
|
|
|
3131
4069
|
lockfileValidation: repo.lockfileValidation
|
|
3132
4070
|
}))
|
|
3133
4071
|
};
|
|
3134
|
-
const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
|
|
3135
|
-
|
|
4072
|
+
const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
|
|
4073
|
+
const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
|
|
4074
|
+
if (hostedEnvironment) {
|
|
3136
4075
|
const workflowGates = saveResult?.workflowGates ?? [];
|
|
3137
|
-
if (effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
|
|
3138
|
-
return { workflowGates };
|
|
3139
|
-
}
|
|
3140
|
-
helpers.write("[save][workflow] Dispatching hosted market deploy gate for deployed resource verification.");
|
|
3141
|
-
const repository = resolveGitHubRepositorySlug(savedRootRepo.path);
|
|
3142
|
-
await dispatchGitHubWorkflowRun(repository, {
|
|
3143
|
-
workflow: "deploy.yml",
|
|
3144
|
-
branch,
|
|
3145
|
-
inputs: {
|
|
3146
|
-
environment: "staging",
|
|
3147
|
-
action_kind: "deploy_web"
|
|
3148
|
-
}
|
|
3149
|
-
});
|
|
3150
|
-
await sleep(5e3);
|
|
3151
|
-
helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
|
|
3152
|
-
const dispatchedGates = await waitForWorkflowGates("save", [
|
|
3153
|
-
hostedDeployGate({
|
|
3154
|
-
name: savedRootRepo.name,
|
|
3155
|
-
repoPath: savedRootRepo.path,
|
|
3156
|
-
repository,
|
|
3157
|
-
workflow: "deploy.yml",
|
|
3158
|
-
branch,
|
|
3159
|
-
headSha: savedRootRepo.commitSha
|
|
3160
|
-
})
|
|
3161
|
-
], "hosted", {
|
|
3162
|
-
root,
|
|
3163
|
-
runId: workflowRun.runId,
|
|
3164
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3165
|
-
});
|
|
3166
4076
|
return {
|
|
3167
|
-
workflowGates
|
|
3168
|
-
|
|
3169
|
-
...dispatchedGates
|
|
3170
|
-
]
|
|
4077
|
+
workflowGates,
|
|
4078
|
+
hostedReconcile: await reconcileSaveHostedEnvironment(root, hostedEnvironment, helpers, workflowRun.runId)
|
|
3171
4079
|
};
|
|
3172
4080
|
}
|
|
3173
4081
|
helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
|
|
@@ -3186,21 +4094,44 @@ async function workflowSave(helpers, input) {
|
|
|
3186
4094
|
branch,
|
|
3187
4095
|
headSha: savedRootRepo.commitSha
|
|
3188
4096
|
})] : [],
|
|
3189
|
-
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) =>
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
4097
|
+
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => {
|
|
4098
|
+
const workflow = hostedWorkflowForSavedRepository(root, repo);
|
|
4099
|
+
const gate = {
|
|
4100
|
+
name: repo.name,
|
|
4101
|
+
repoPath: repo.path,
|
|
4102
|
+
workflow,
|
|
4103
|
+
branch: String(repo.branch),
|
|
4104
|
+
headSha: String(repo.commitSha)
|
|
4105
|
+
};
|
|
4106
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
4107
|
+
})
|
|
3196
4108
|
], "hosted", {
|
|
3197
4109
|
root,
|
|
3198
4110
|
runId: workflowRun.runId,
|
|
3199
4111
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3200
4112
|
}).then((workflowGates) => ({ workflowGates }));
|
|
3201
4113
|
}) : { workflowGates: [] };
|
|
3202
|
-
const
|
|
3203
|
-
helpers.write("[save][workflow] Running
|
|
4114
|
+
const releaseProof = saveLane === "promotion" && process.env.TREESEED_STAGE_WAIT_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-proof", async () => {
|
|
4115
|
+
helpers.write("[save][workflow] Running authoritative hosted release proof.");
|
|
4116
|
+
const proof = await runTreeseedProof({
|
|
4117
|
+
root,
|
|
4118
|
+
target: scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
|
|
4119
|
+
driver: "github-hosted",
|
|
4120
|
+
write: (line, stream) => helpers.write(line, stream)
|
|
4121
|
+
});
|
|
4122
|
+
if (proof.failures.length > 0) {
|
|
4123
|
+
const first = proof.failures[0];
|
|
4124
|
+
workflowError("save", "validation_failed", [
|
|
4125
|
+
"Treeseed promotion proof failed.",
|
|
4126
|
+
`- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
|
|
4127
|
+
first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
|
|
4128
|
+
"Hosted GitHub workflow proof is authoritative; local action simulation is advisory."
|
|
4129
|
+
].filter(Boolean).join("\n"), { details: { proof } });
|
|
4130
|
+
}
|
|
4131
|
+
return proof;
|
|
4132
|
+
}) : saveLane === "promotion" ? (skipJournalStep(root, workflowRun.runId, "release-proof", { skippedReason: "disabled" }), { skipped: true, reason: "disabled" }) : null;
|
|
4133
|
+
const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
|
|
4134
|
+
helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
|
|
3204
4135
|
const releaseSession = resolveTreeseedWorkflowSession(root);
|
|
3205
4136
|
const stagingReleasePlan = buildReleasePlanSnapshot({
|
|
3206
4137
|
root,
|
|
@@ -3211,28 +4142,42 @@ async function workflowSave(helpers, input) {
|
|
|
3211
4142
|
rootRepo: savedRootRepo,
|
|
3212
4143
|
blockers: []
|
|
3213
4144
|
});
|
|
3214
|
-
return
|
|
4145
|
+
return runReleaseCandidateProofForPlan("save", root, stagingReleasePlan, {
|
|
4146
|
+
mode: releaseCandidateMode,
|
|
4147
|
+
lane: saveLane,
|
|
4148
|
+
write: (line, stream) => helpers.write(line, stream)
|
|
4149
|
+
});
|
|
3215
4150
|
}) : null;
|
|
3216
4151
|
let previewAction = { status: "skipped" };
|
|
3217
4152
|
if (beforeState.branchRole === "feature" && branch) {
|
|
3218
4153
|
if (effectiveInput.preview === true) {
|
|
3219
4154
|
previewAction = {
|
|
3220
4155
|
status: previewInitialized ? "refreshed" : "created",
|
|
3221
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () =>
|
|
4156
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
|
|
3222
4157
|
};
|
|
3223
4158
|
} else if (effectiveInput.refreshPreview !== false && previewInitialized) {
|
|
3224
4159
|
previewAction = {
|
|
3225
4160
|
status: "refreshed",
|
|
3226
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () =>
|
|
4161
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
3227
4162
|
};
|
|
3228
4163
|
}
|
|
3229
4164
|
}
|
|
3230
|
-
const
|
|
4165
|
+
const applicationSelection = selectWorkflowApplications(root, {
|
|
4166
|
+
packageSelection: {
|
|
4167
|
+
selected: savedPackageReports.map((report) => report.name)
|
|
4168
|
+
}
|
|
4169
|
+
});
|
|
4170
|
+
const hostingAudit = await workflowHostedVerificationGateRequired(
|
|
3231
4171
|
"save",
|
|
3232
4172
|
root,
|
|
3233
4173
|
helpers,
|
|
3234
4174
|
scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
|
|
3235
|
-
{
|
|
4175
|
+
{
|
|
4176
|
+
enabled: effectiveInput.verifyDeployedResources === true,
|
|
4177
|
+
strict: true,
|
|
4178
|
+
live: effectiveInput.verifyDeployedResources === true,
|
|
4179
|
+
appId: singleSelectedWorkflowAppId(applicationSelection)
|
|
4180
|
+
}
|
|
3236
4181
|
);
|
|
3237
4182
|
const payload = {
|
|
3238
4183
|
mode: saveResult?.mode ?? mode,
|
|
@@ -3257,10 +4202,15 @@ async function workflowSave(helpers, input) {
|
|
|
3257
4202
|
workspaceLinks,
|
|
3258
4203
|
commandReadiness,
|
|
3259
4204
|
lockfileValidation,
|
|
3260
|
-
ciMode:
|
|
4205
|
+
ciMode: saveCiMode,
|
|
4206
|
+
lane: saveLane,
|
|
3261
4207
|
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
4208
|
+
releaseCandidateMode,
|
|
4209
|
+
applicationSelection,
|
|
3262
4210
|
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
4211
|
+
hostedReconcile: saveWorkflowGates?.hostedReconcile ?? null,
|
|
3263
4212
|
releaseCandidate,
|
|
4213
|
+
releaseProof,
|
|
3264
4214
|
hostingAudit,
|
|
3265
4215
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3266
4216
|
};
|
|
@@ -3342,17 +4292,17 @@ async function workflowClose(helpers, input) {
|
|
|
3342
4292
|
failure: planAutoResumeRun.failure
|
|
3343
4293
|
} : null,
|
|
3344
4294
|
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3345
|
-
autoSaveRequired: session.rootRepo.dirty || session.
|
|
3346
|
-
repos:
|
|
4295
|
+
autoSaveRequired: session.rootRepo.dirty || session.managedRepos.some((repo) => repo.dirty),
|
|
4296
|
+
repos: createManagedWorkflowRepoReports(root),
|
|
3347
4297
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
3348
4298
|
blockers,
|
|
3349
4299
|
plannedSteps: [
|
|
3350
4300
|
{ id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
|
|
3351
4301
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
3352
4302
|
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
3353
|
-
...
|
|
3354
|
-
id: `cleanup-${
|
|
3355
|
-
description: `Archive and delete ${branchName ?? "(current task)"} in ${
|
|
4303
|
+
...checkedOutManagedWorkflowRepos(root).map((repo) => ({
|
|
4304
|
+
id: `cleanup-${repo.name}`,
|
|
4305
|
+
description: `Archive and delete ${branchName ?? "(current task)"} in ${repo.name}`
|
|
3356
4306
|
})),
|
|
3357
4307
|
{ id: "workspace-link", description: "Restore local workspace links on the final branch" },
|
|
3358
4308
|
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
|
|
@@ -3374,6 +4324,7 @@ async function workflowClose(helpers, input) {
|
|
|
3374
4324
|
const featureBranch = assertFeatureBranch(root);
|
|
3375
4325
|
const mode = activeSession.mode;
|
|
3376
4326
|
const repoDir = activeSession.gitRoot;
|
|
4327
|
+
const managedWorktreeForClose = isManagedWorkflowWorktree(root);
|
|
3377
4328
|
assertSessionBranchSafety("close", activeSession);
|
|
3378
4329
|
if (mode === "recursive-workspace") {
|
|
3379
4330
|
assertWorkspaceClean(root);
|
|
@@ -3381,7 +4332,7 @@ async function workflowClose(helpers, input) {
|
|
|
3381
4332
|
assertCleanWorktree(root);
|
|
3382
4333
|
}
|
|
3383
4334
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3384
|
-
const packageReports =
|
|
4335
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
3385
4336
|
const workflowRun = acquireWorkflowRun(
|
|
3386
4337
|
"close",
|
|
3387
4338
|
activeSession,
|
|
@@ -3419,30 +4370,34 @@ async function workflowClose(helpers, input) {
|
|
|
3419
4370
|
}
|
|
3420
4371
|
try {
|
|
3421
4372
|
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
3422
|
-
const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () =>
|
|
4373
|
+
const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyWorkflowBranchPreviewIfPresent(root, featureBranch, helpers.context));
|
|
3423
4374
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
3424
|
-
const
|
|
4375
|
+
const head = updateHead(repoDir);
|
|
4376
|
+
const tag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
3425
4377
|
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
3426
|
-
|
|
3427
|
-
|
|
4378
|
+
if (!managedWorktreeForClose) {
|
|
4379
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
4380
|
+
}
|
|
4381
|
+
if (effectiveInput.deleteBranch !== false && !managedWorktreeForClose) {
|
|
3428
4382
|
deleteLocalBranch(repoDir, featureBranch);
|
|
3429
4383
|
}
|
|
3430
4384
|
return {
|
|
3431
|
-
|
|
4385
|
+
head,
|
|
4386
|
+
tagName: tag.tagName,
|
|
3432
4387
|
deletedRemote,
|
|
3433
|
-
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
3434
|
-
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
4388
|
+
deletedLocal: effectiveInput.deleteBranch !== false && !managedWorktreeForClose,
|
|
4389
|
+
branch: managedWorktreeForClose ? featureBranch : currentBranch(repoDir) || STAGING_BRANCH,
|
|
3435
4390
|
dirty: hasMeaningfulChanges(repoDir)
|
|
3436
4391
|
};
|
|
3437
4392
|
});
|
|
3438
|
-
rootRepo.tagName =
|
|
3439
|
-
rootRepo.commitSha = String(rootCleanup?.
|
|
4393
|
+
rootRepo.tagName = typeof rootCleanup?.tagName === "string" ? rootCleanup.tagName : null;
|
|
4394
|
+
rootRepo.commitSha = String(rootCleanup?.head ?? rootRepo.commitSha ?? "");
|
|
3440
4395
|
rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
|
|
3441
4396
|
rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
|
|
3442
4397
|
rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3443
4398
|
rootRepo.dirty = rootCleanup?.dirty === true;
|
|
3444
|
-
for (const
|
|
3445
|
-
const report = findReportByName(packageReports,
|
|
4399
|
+
for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
|
|
4400
|
+
const report = findReportByName(packageReports, managedRepo.name);
|
|
3446
4401
|
if (!report) {
|
|
3447
4402
|
continue;
|
|
3448
4403
|
}
|
|
@@ -3453,16 +4408,21 @@ async function workflowClose(helpers, input) {
|
|
|
3453
4408
|
Object.assign(report, cleanup);
|
|
3454
4409
|
}
|
|
3455
4410
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
3456
|
-
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
4411
|
+
const finalBranch = managedWorktreeForClose ? STAGING_BRANCH : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3457
4412
|
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
3458
|
-
const worktreeCleanup =
|
|
4413
|
+
const worktreeCleanup = managedWorktreeForClose ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root, {
|
|
4414
|
+
deleteBranch: effectiveInput.deleteBranch !== false
|
|
4415
|
+
})) : { removed: false, reason: "not-managed" };
|
|
4416
|
+
if (worktreeCleanup.deletedLocalBranch === true) {
|
|
4417
|
+
rootRepo.deletedLocal = true;
|
|
4418
|
+
rootRepo.branch = STAGING_BRANCH;
|
|
4419
|
+
}
|
|
3459
4420
|
const payload = {
|
|
3460
4421
|
mode,
|
|
3461
4422
|
branchName: featureBranch,
|
|
3462
4423
|
message,
|
|
3463
4424
|
autoSaved: autoSave.performed,
|
|
3464
4425
|
autoSaveResult: autoSave.save,
|
|
3465
|
-
deprecatedTag: rootCleanup?.deprecatedTag ?? null,
|
|
3466
4426
|
repos: packageReports,
|
|
3467
4427
|
rootRepo,
|
|
3468
4428
|
previewCleanup,
|
|
@@ -3483,6 +4443,7 @@ async function workflowClose(helpers, input) {
|
|
|
3483
4443
|
payload,
|
|
3484
4444
|
{
|
|
3485
4445
|
runId: workflowRun.runId,
|
|
4446
|
+
includeFinalState: !managedWorktreeForClose,
|
|
3486
4447
|
nextSteps: createNextSteps([
|
|
3487
4448
|
{ operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
|
|
3488
4449
|
])
|
|
@@ -3505,345 +4466,460 @@ async function workflowClose(helpers, input) {
|
|
|
3505
4466
|
toError("close", error);
|
|
3506
4467
|
}
|
|
3507
4468
|
}
|
|
4469
|
+
function normalizeStageVerifyMode(value) {
|
|
4470
|
+
return value === "local" || value === "none" ? value : "action";
|
|
4471
|
+
}
|
|
4472
|
+
function normalizeStageCiMode(input) {
|
|
4473
|
+
if (input.ciMode === "hosted" || input.waitForStaging === true) return "hosted";
|
|
4474
|
+
return "off";
|
|
4475
|
+
}
|
|
4476
|
+
function normalizeStageCleanupMode(input) {
|
|
4477
|
+
if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
|
|
4478
|
+
return "manual";
|
|
4479
|
+
}
|
|
4480
|
+
function stageCandidateManifestPath(root, runId) {
|
|
4481
|
+
return {
|
|
4482
|
+
latest: resolve(root, ".treeseed", "workflow", "stage-candidates", "latest.json"),
|
|
4483
|
+
run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
function writeStageCandidateManifest(root, runId, manifest) {
|
|
4487
|
+
const paths = stageCandidateManifestPath(root, runId);
|
|
4488
|
+
for (const filePath of [paths.latest, paths.run]) {
|
|
4489
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
4490
|
+
writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
|
|
4491
|
+
`, "utf8");
|
|
4492
|
+
}
|
|
4493
|
+
return manifest;
|
|
4494
|
+
}
|
|
4495
|
+
function checkedOutStagePackageRepos(root) {
|
|
4496
|
+
return checkedOutManagedWorkflowRepos(root).filter((repo) => repo.kind === "package");
|
|
4497
|
+
}
|
|
4498
|
+
function buildStagePromotionPlan(root, branchName, input) {
|
|
4499
|
+
const gitRoot = repoRoot(root);
|
|
4500
|
+
const repos = [
|
|
4501
|
+
...checkedOutStagePackageRepos(root).map((repo) => ({
|
|
4502
|
+
name: repo.name,
|
|
4503
|
+
path: repo.dir,
|
|
4504
|
+
kind: "managed",
|
|
4505
|
+
sourceBranch: branchName,
|
|
4506
|
+
targetBranch: STAGING_BRANCH,
|
|
4507
|
+
remoteSourceExists: remoteBranchExists(repo.dir, branchName),
|
|
4508
|
+
beforeHead: branchExists(repo.dir, branchName) ? headCommit(repo.dir, branchName) : null,
|
|
4509
|
+
stagingHeadBefore: remoteBranchExists(repo.dir, STAGING_BRANCH) ? remoteHeadCommit(repo.dir, STAGING_BRANCH) : null
|
|
4510
|
+
})),
|
|
4511
|
+
{
|
|
4512
|
+
name: "@treeseed/market",
|
|
4513
|
+
path: gitRoot,
|
|
4514
|
+
kind: "root",
|
|
4515
|
+
sourceBranch: branchName,
|
|
4516
|
+
targetBranch: STAGING_BRANCH,
|
|
4517
|
+
remoteSourceExists: remoteBranchExists(gitRoot, branchName),
|
|
4518
|
+
beforeHead: branchExists(gitRoot, branchName) ? headCommit(gitRoot, branchName) : null,
|
|
4519
|
+
stagingHeadBefore: remoteBranchExists(gitRoot, STAGING_BRANCH) ? remoteHeadCommit(gitRoot, STAGING_BRANCH) : null
|
|
4520
|
+
}
|
|
4521
|
+
];
|
|
4522
|
+
return {
|
|
4523
|
+
schemaVersion: 1,
|
|
4524
|
+
branchName,
|
|
4525
|
+
targetBranch: STAGING_BRANCH,
|
|
4526
|
+
updateFrom: input.updateFrom,
|
|
4527
|
+
verifyMode: input.verifyMode,
|
|
4528
|
+
ciMode: input.ciMode,
|
|
4529
|
+
cleanupMode: input.cleanupMode,
|
|
4530
|
+
repos,
|
|
4531
|
+
phases: [
|
|
4532
|
+
"preflight",
|
|
4533
|
+
"merge-staging-down",
|
|
4534
|
+
"save-integrated-feature",
|
|
4535
|
+
"verify-integrated-feature",
|
|
4536
|
+
"promote-to-staging",
|
|
4537
|
+
"verify-staging-refs",
|
|
4538
|
+
"workspace-link-restore",
|
|
4539
|
+
"cleanup-source"
|
|
4540
|
+
]
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
function stagePreflightBlockers(root, branchName, plan) {
|
|
4544
|
+
const blockers = [];
|
|
4545
|
+
if (!branchName || branchName === STAGING_BRANCH || branchName === PRODUCTION_BRANCH) {
|
|
4546
|
+
blockers.push(`stage requires a feature branch; current branch is ${branchName || "(none)"}.`);
|
|
4547
|
+
}
|
|
4548
|
+
for (const repo of plan.repos) {
|
|
4549
|
+
const branch = currentBranch(repo.path) || null;
|
|
4550
|
+
if (hasMeaningfulChanges(repo.path)) {
|
|
4551
|
+
blockers.push(`${repo.name} has uncommitted changes.`);
|
|
4552
|
+
}
|
|
4553
|
+
if (branch !== branchName && repo.kind === "managed" && repo.remoteSourceExists) {
|
|
4554
|
+
blockers.push(`${repo.name} is on ${branch ?? "(detached)"} instead of ${branchName}.`);
|
|
4555
|
+
}
|
|
4556
|
+
if (repo.kind === "root" && branch !== branchName) {
|
|
4557
|
+
blockers.push(`${repo.name} is on ${branch ?? "(detached)"} instead of ${branchName}.`);
|
|
4558
|
+
}
|
|
4559
|
+
try {
|
|
4560
|
+
originRemoteUrl(repo.path);
|
|
4561
|
+
} catch {
|
|
4562
|
+
blockers.push(`${repo.name} has no readable origin remote.`);
|
|
4563
|
+
}
|
|
4564
|
+
if (repo.kind === "root" && !repo.remoteSourceExists) {
|
|
4565
|
+
blockers.push(`${repo.name} feature branch ${branchName} has not been pushed to origin.`);
|
|
4566
|
+
}
|
|
4567
|
+
if (repo.kind === "managed" && branchExists(repo.path, branchName) && repo.remoteSourceExists) {
|
|
4568
|
+
const localHead = headCommit(repo.path, branchName);
|
|
4569
|
+
const remoteHead = remoteHeadCommit(repo.path, branchName);
|
|
4570
|
+
if (localHead !== remoteHead) {
|
|
4571
|
+
blockers.push(`${repo.name} local ${branchName} (${localHead.slice(0, 12)}) does not match origin/${branchName} (${remoteHead.slice(0, 12)}). Run save first.`);
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
return blockers;
|
|
4576
|
+
}
|
|
4577
|
+
function stageConflictError(message, details) {
|
|
4578
|
+
return new TreeseedWorkflowError("stage", "merge_conflict", message, {
|
|
4579
|
+
details,
|
|
4580
|
+
exitCode: 12
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
function createStageCandidateManifest(root, runId, branchName, plan, verification) {
|
|
4584
|
+
const gitRoot = repoRoot(root);
|
|
4585
|
+
const packageRepos = plan.repos.filter((repo) => repo.kind === "managed" && repo.remoteSourceExists);
|
|
4586
|
+
return {
|
|
4587
|
+
schemaVersion: 1,
|
|
4588
|
+
kind: "treeseed.stage-candidate",
|
|
4589
|
+
runId,
|
|
4590
|
+
branchName,
|
|
4591
|
+
targetBranch: STAGING_BRANCH,
|
|
4592
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4593
|
+
root: {
|
|
4594
|
+
repo: "@treeseed/market",
|
|
4595
|
+
commit: headCommit(gitRoot),
|
|
4596
|
+
verified: verification.status === "passed" || verification.status === "skipped"
|
|
4597
|
+
},
|
|
4598
|
+
packages: packageRepos.map((repo) => ({
|
|
4599
|
+
name: repo.name,
|
|
4600
|
+
path: repo.path,
|
|
4601
|
+
commit: headCommit(repo.path),
|
|
4602
|
+
remote: (() => {
|
|
4603
|
+
try {
|
|
4604
|
+
return originRemoteUrl(repo.path);
|
|
4605
|
+
} catch {
|
|
4606
|
+
return null;
|
|
4607
|
+
}
|
|
4608
|
+
})(),
|
|
4609
|
+
verified: verification.status === "passed" || verification.status === "skipped"
|
|
4610
|
+
})),
|
|
4611
|
+
verification,
|
|
4612
|
+
stagingHeadsBefore: Object.fromEntries(plan.repos.map((repo) => [repo.name, repo.stagingHeadBefore]))
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
function cleanupStageSourceBranches(root, branchName, manifest) {
|
|
4616
|
+
const results = [];
|
|
4617
|
+
for (const repo of checkedOutStagePackageRepos(root)) {
|
|
4618
|
+
const manifestRepo = manifest.packages.find((entry) => entry.name === repo.name);
|
|
4619
|
+
if (!manifestRepo) continue;
|
|
4620
|
+
const remoteDeleted = deleteRemoteBranch(repo.dir, branchName);
|
|
4621
|
+
if ((currentBranch(repo.dir) || null) === branchName) {
|
|
4622
|
+
syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
|
|
4623
|
+
}
|
|
4624
|
+
const localExists = branchExists(repo.dir, branchName);
|
|
4625
|
+
if (localExists && (currentBranch(repo.dir) || null) !== branchName) {
|
|
4626
|
+
deleteLocalBranch(repo.dir, branchName);
|
|
4627
|
+
}
|
|
4628
|
+
results.push({
|
|
4629
|
+
name: repo.name,
|
|
4630
|
+
path: repo.dir,
|
|
4631
|
+
remoteDeleted,
|
|
4632
|
+
localDeleted: localExists && !branchExists(repo.dir, branchName)
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
const gitRoot = repoRoot(root);
|
|
4636
|
+
const rootRemoteDeleted = deleteRemoteBranch(gitRoot, branchName);
|
|
4637
|
+
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
4638
|
+
const worktreeCleanup = managedWorktree ? removeManagedWorkflowWorktree(root, { deleteBranch: false }) : { removed: false, reason: "not-managed" };
|
|
4639
|
+
const branchDeletionRoot = managedWorktree?.primaryRoot ? repoRoot(managedWorktree.primaryRoot) : gitRoot;
|
|
4640
|
+
if (!managedWorktree && (currentBranch(gitRoot) || null) === branchName) {
|
|
4641
|
+
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
4642
|
+
}
|
|
4643
|
+
const rootLocalExists = branchExists(branchDeletionRoot, branchName);
|
|
4644
|
+
if (rootLocalExists && (currentBranch(branchDeletionRoot) || null) !== branchName) {
|
|
4645
|
+
deleteLocalBranch(branchDeletionRoot, branchName);
|
|
4646
|
+
}
|
|
4647
|
+
results.push({
|
|
4648
|
+
name: "@treeseed/market",
|
|
4649
|
+
path: branchDeletionRoot,
|
|
4650
|
+
remoteDeleted: rootRemoteDeleted,
|
|
4651
|
+
localDeleted: rootLocalExists && !branchExists(branchDeletionRoot, branchName)
|
|
4652
|
+
});
|
|
4653
|
+
return {
|
|
4654
|
+
status: "completed",
|
|
4655
|
+
repos: results,
|
|
4656
|
+
worktreeCleanup
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
3508
4659
|
async function workflowStage(helpers, input) {
|
|
3509
4660
|
try {
|
|
3510
4661
|
return await withContextEnv(helpers.context.env, async () => {
|
|
3511
4662
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
3512
4663
|
const root = workspaceRoot(tenantRoot);
|
|
3513
4664
|
const executionMode = normalizeExecutionMode(input);
|
|
3514
|
-
const
|
|
4665
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3515
4666
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3516
|
-
const
|
|
3517
|
-
const
|
|
4667
|
+
const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
|
|
4668
|
+
const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
|
|
4669
|
+
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
|
|
3518
4670
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
3519
4671
|
const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
|
|
3520
|
-
|
|
3521
|
-
|
|
4672
|
+
if (effectiveInput.verifyDeployedResources === true) {
|
|
4673
|
+
workflowError("stage", "validation_failed", "Stage no longer verifies deployed resources. Promote refs with stage, then run staging release/hosting verification separately.");
|
|
4674
|
+
}
|
|
4675
|
+
const verifyMode = normalizeStageVerifyMode(effectiveInput.verifyMode);
|
|
4676
|
+
const ciMode = normalizeStageCiMode(effectiveInput);
|
|
4677
|
+
const cleanupMode = normalizeStageCleanupMode(effectiveInput);
|
|
4678
|
+
const updateFrom = effectiveInput.updateFrom ?? STAGING_BRANCH;
|
|
4679
|
+
if (updateFrom !== STAGING_BRANCH) {
|
|
4680
|
+
workflowError("stage", "validation_failed", `Stage currently supports only --update-from ${STAGING_BRANCH}. Received ${updateFrom}.`);
|
|
4681
|
+
}
|
|
4682
|
+
const applicationSelection = selectWorkflowApplications(root, { packageSelection: session.packageSelection });
|
|
4683
|
+
const featureBranch = executionMode === "execute" ? assertFeatureBranch(root) : session.branchName ?? "";
|
|
4684
|
+
let plan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
|
|
4685
|
+
let blockers = stagePreflightBlockers(root, featureBranch, plan);
|
|
4686
|
+
const basePayload = {
|
|
4687
|
+
mode: "stage-promotion",
|
|
4688
|
+
branchName: featureBranch,
|
|
4689
|
+
branchRole: session.branchRole,
|
|
4690
|
+
mergeTarget: STAGING_BRANCH,
|
|
4691
|
+
mergeStrategy: "merge-staging-down-then-exact-sha",
|
|
4692
|
+
message,
|
|
4693
|
+
verifyMode,
|
|
4694
|
+
ciMode,
|
|
4695
|
+
cleanupMode,
|
|
4696
|
+
updateFrom,
|
|
4697
|
+
waitForStaging: ciMode === "hosted",
|
|
4698
|
+
applicationSelection,
|
|
4699
|
+
plan,
|
|
4700
|
+
phases: plan.phases,
|
|
4701
|
+
blockers,
|
|
4702
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
4703
|
+
runId: planAutoResumeRun.runId,
|
|
4704
|
+
branch: planAutoResumeRun.session.branchName,
|
|
4705
|
+
failure: planAutoResumeRun.failure
|
|
4706
|
+
} : null,
|
|
4707
|
+
legacyMutationPathDisabled: true,
|
|
4708
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
4709
|
+
};
|
|
3522
4710
|
if (executionMode === "plan") {
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
blockers.
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
}
|
|
3532
|
-
return buildWorkflowResult(
|
|
3533
|
-
"stage",
|
|
3534
|
-
root,
|
|
3535
|
-
{
|
|
3536
|
-
mode: initialSession.mode,
|
|
3537
|
-
branchName: initialSession.branchName,
|
|
3538
|
-
mergeTarget: STAGING_BRANCH,
|
|
3539
|
-
mergeStrategy: "squash",
|
|
3540
|
-
message,
|
|
3541
|
-
ciMode,
|
|
3542
|
-
autoResumeCandidate: planAutoResumeRun ? {
|
|
3543
|
-
runId: planAutoResumeRun.runId,
|
|
3544
|
-
branch: planAutoResumeRun.session.branchName,
|
|
3545
|
-
failure: planAutoResumeRun.failure
|
|
3546
|
-
} : null,
|
|
3547
|
-
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3548
|
-
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
3549
|
-
blockers,
|
|
3550
|
-
rootRepo: createWorkspaceRootRepoReport(root),
|
|
3551
|
-
repos: createWorkspacePackageReports(root),
|
|
3552
|
-
plannedSteps: [
|
|
3553
|
-
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
3554
|
-
id: `merge-${pkg.name}`,
|
|
3555
|
-
description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
|
|
3556
|
-
})),
|
|
3557
|
-
{ id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
|
|
3558
|
-
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
3559
|
-
{ id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
|
|
3560
|
-
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
|
|
3561
|
-
{ id: "release-candidate", description: "Run release-candidate readiness checks for the exact staging state" },
|
|
3562
|
-
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
3563
|
-
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
3564
|
-
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
3565
|
-
id: `cleanup-${pkg.name}`,
|
|
3566
|
-
description: `Archive and delete the task branch from ${pkg.name}`
|
|
3567
|
-
})),
|
|
3568
|
-
{ id: "workspace-link", description: "Restore local workspace links on staging" }
|
|
3569
|
-
]
|
|
3570
|
-
},
|
|
3571
|
-
{
|
|
3572
|
-
executionMode,
|
|
3573
|
-
nextSteps: createNextSteps([
|
|
3574
|
-
{ operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
|
|
3575
|
-
])
|
|
3576
|
-
}
|
|
3577
|
-
);
|
|
3578
|
-
}
|
|
3579
|
-
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
3580
|
-
message,
|
|
3581
|
-
autoSave: effectiveInput.autoSave
|
|
3582
|
-
});
|
|
3583
|
-
const session = resolveTreeseedWorkflowSession(root);
|
|
3584
|
-
const featureBranch = assertFeatureBranch(root);
|
|
3585
|
-
const mode = session.mode;
|
|
3586
|
-
assertSessionBranchSafety("stage", session);
|
|
3587
|
-
if (mode === "recursive-workspace") {
|
|
3588
|
-
assertWorkspaceClean(root);
|
|
3589
|
-
} else {
|
|
3590
|
-
assertCleanWorktree(root);
|
|
4711
|
+
return buildWorkflowResult("stage", root, basePayload, {
|
|
4712
|
+
executionMode,
|
|
4713
|
+
summary: blockers.length > 0 ? "Treeseed stage plan blocked." : "Treeseed stage promotion plan ready.",
|
|
4714
|
+
includeFinalState: false,
|
|
4715
|
+
nextSteps: createNextSteps([
|
|
4716
|
+
blockers.length > 0 ? { operation: "status", reason: "Resolve blockers before staging." } : { operation: "stage", reason: "Promote the verified feature branch to staging.", input: { message } }
|
|
4717
|
+
])
|
|
4718
|
+
});
|
|
3591
4719
|
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
validateStagingWorkflowContracts(root);
|
|
3595
|
-
runWorkspaceSavePreflight({ cwd: root });
|
|
3596
|
-
const repoDir = session.gitRoot;
|
|
3597
|
-
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3598
|
-
const packageReports = createWorkspacePackageReports(root);
|
|
3599
|
-
const workflowRun = acquireWorkflowRun(
|
|
3600
|
-
"stage",
|
|
3601
|
-
session,
|
|
3602
|
-
{
|
|
4720
|
+
if (effectiveInput.autoSave === true) {
|
|
4721
|
+
await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
3603
4722
|
message,
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
},
|
|
3611
|
-
[
|
|
3612
|
-
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3613
|
-
...packageReports.map((report) => ({
|
|
3614
|
-
id: `merge-${report.name}`,
|
|
3615
|
-
description: `Merge ${featureBranch} into ${report.name} staging`,
|
|
3616
|
-
repoName: report.name,
|
|
3617
|
-
repoPath: report.path,
|
|
3618
|
-
branch: featureBranch,
|
|
3619
|
-
resumable: true
|
|
3620
|
-
})),
|
|
3621
|
-
{ id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3622
|
-
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3623
|
-
{ id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3624
|
-
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3625
|
-
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3626
|
-
...packageReports.map((report) => ({
|
|
3627
|
-
id: `cleanup-${report.name}`,
|
|
3628
|
-
description: `Archive ${featureBranch} in ${report.name}`,
|
|
3629
|
-
repoName: report.name,
|
|
3630
|
-
repoPath: report.path,
|
|
3631
|
-
branch: featureBranch,
|
|
3632
|
-
resumable: true
|
|
3633
|
-
})),
|
|
3634
|
-
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3635
|
-
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
3636
|
-
],
|
|
3637
|
-
autoResumeRun ? {
|
|
3638
|
-
...helpers.context,
|
|
3639
|
-
workflow: {
|
|
3640
|
-
...helpers.context.workflow ?? {},
|
|
3641
|
-
resumeRunId: autoResumeRun.runId
|
|
3642
|
-
}
|
|
3643
|
-
} : helpers.context
|
|
3644
|
-
);
|
|
3645
|
-
if (autoResumeRun) {
|
|
3646
|
-
helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
4723
|
+
autoSave: true,
|
|
4724
|
+
verify: false,
|
|
4725
|
+
preview: false
|
|
4726
|
+
});
|
|
4727
|
+
plan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
|
|
4728
|
+
blockers = stagePreflightBlockers(root, featureBranch, plan);
|
|
3647
4729
|
}
|
|
4730
|
+
if (blockers.length > 0) {
|
|
4731
|
+
workflowError("stage", "validation_failed", `stage is blocked:
|
|
4732
|
+
${blockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
4733
|
+
details: { blockers, plan }
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
4736
|
+
const workflowRun = acquireWorkflowRun("stage", resolveTreeseedWorkflowSession(root), {
|
|
4737
|
+
...effectiveInput,
|
|
4738
|
+
verifyMode,
|
|
4739
|
+
ciMode,
|
|
4740
|
+
cleanupMode,
|
|
4741
|
+
updateFrom
|
|
4742
|
+
}, [
|
|
4743
|
+
{ id: "preflight", description: "Validate clean feature branch before staging", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4744
|
+
{ id: "merge-staging-down", description: "Merge staging into feature branches", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4745
|
+
{ id: "save-integrated-feature", description: "Save integrated feature state after staging merge-down", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4746
|
+
{ id: "verify-integrated-feature", description: "Run local proof before staging mutation", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4747
|
+
{ id: "write-stage-candidate", description: "Write exact stage candidate manifest", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4748
|
+
{ id: "promote-to-staging", description: "Promote exact verified refs to staging", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
|
|
4749
|
+
{ id: "verify-staging-refs", description: "Verify remote staging refs match promoted commits", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
|
|
4750
|
+
{ id: "hosted-ci", description: "Wait for hosted staging CI when explicitly requested", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
|
|
4751
|
+
{ id: "workspace-link-restore", description: "Restore local workspace links after stage", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
|
|
4752
|
+
{ id: "cleanup-source", description: "Clean up source branches and worktree after successful promotion", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true }
|
|
4753
|
+
], helpers.context);
|
|
3648
4754
|
try {
|
|
3649
|
-
await executeJournalStep(root, workflowRun.runId, "
|
|
3650
|
-
|
|
3651
|
-
const
|
|
3652
|
-
if (
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
|
|
3658
|
-
continue;
|
|
4755
|
+
await executeJournalStep(root, workflowRun.runId, "preflight", () => {
|
|
4756
|
+
const currentPlan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
|
|
4757
|
+
const currentBlockers = stagePreflightBlockers(root, featureBranch, currentPlan);
|
|
4758
|
+
if (currentBlockers.length > 0) {
|
|
4759
|
+
workflowError("stage", "validation_failed", `stage is blocked:
|
|
4760
|
+
${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
4761
|
+
details: { blockers: currentBlockers, plan: currentPlan }
|
|
4762
|
+
});
|
|
3659
4763
|
}
|
|
4764
|
+
return { status: "passed", checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4765
|
+
});
|
|
4766
|
+
const mergeDown = await executeJournalStep(root, workflowRun.runId, "merge-staging-down", () => {
|
|
4767
|
+
const results = [];
|
|
3660
4768
|
try {
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
4769
|
+
for (const repo of checkedOutStagePackageRepos(root)) {
|
|
4770
|
+
if (!remoteBranchExists(repo.dir, featureBranch)) {
|
|
4771
|
+
results.push({ name: repo.name, path: repo.dir, skipped: true, reason: "remote-branch-missing" });
|
|
4772
|
+
continue;
|
|
4773
|
+
}
|
|
4774
|
+
results.push({
|
|
4775
|
+
name: repo.name,
|
|
4776
|
+
path: repo.dir,
|
|
4777
|
+
...mergeBranchDownIntoFeature(repo.dir, {
|
|
4778
|
+
featureBranch,
|
|
4779
|
+
sourceBranch: STAGING_BRANCH,
|
|
4780
|
+
message: `stage: merge ${STAGING_BRANCH} into ${featureBranch}`,
|
|
4781
|
+
allowGeneratedMetadataAutoResolution: true
|
|
4782
|
+
})
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
results.push({
|
|
4786
|
+
name: "@treeseed/market",
|
|
4787
|
+
path: repoRoot(root),
|
|
4788
|
+
...mergeBranchDownIntoFeature(repoRoot(root), {
|
|
4789
|
+
featureBranch,
|
|
4790
|
+
sourceBranch: STAGING_BRANCH,
|
|
4791
|
+
message: `stage: merge ${STAGING_BRANCH} into ${featureBranch}`,
|
|
4792
|
+
allowGeneratedMetadataAutoResolution: true
|
|
4793
|
+
})
|
|
4794
|
+
});
|
|
3669
4795
|
} catch (error) {
|
|
3670
|
-
const
|
|
3671
|
-
throw
|
|
3672
|
-
details
|
|
3673
|
-
|
|
4796
|
+
const details = error && typeof error === "object" ? error : {};
|
|
4797
|
+
throw stageConflictError(error instanceof Error ? error.message : String(error), {
|
|
4798
|
+
...details,
|
|
4799
|
+
results,
|
|
4800
|
+
branchName: featureBranch,
|
|
4801
|
+
targetBranch: STAGING_BRANCH
|
|
3674
4802
|
});
|
|
3675
4803
|
}
|
|
3676
|
-
|
|
3677
|
-
let rootMerge = null;
|
|
3678
|
-
try {
|
|
3679
|
-
rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
|
|
3680
|
-
assertCleanWorktree(root);
|
|
3681
|
-
if (isManagedWorkflowWorktree(root)) {
|
|
3682
|
-
checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
|
|
3683
|
-
} else {
|
|
3684
|
-
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
3685
|
-
}
|
|
3686
|
-
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
3687
|
-
if (mode === "recursive-workspace") {
|
|
3688
|
-
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
3689
|
-
}
|
|
3690
|
-
const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
|
|
3691
|
-
root,
|
|
3692
|
-
gitRoot: repoDir,
|
|
3693
|
-
branch: STAGING_BRANCH,
|
|
3694
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3695
|
-
});
|
|
3696
|
-
if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
|
|
3697
|
-
run("git", ["add", "-A"], { cwd: repoDir });
|
|
3698
|
-
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
3699
|
-
}
|
|
3700
|
-
if (isManagedWorkflowWorktree(root)) {
|
|
3701
|
-
pushHeadToBranch(repoDir, STAGING_BRANCH);
|
|
3702
|
-
} else {
|
|
3703
|
-
pushBranch(repoDir, STAGING_BRANCH);
|
|
3704
|
-
}
|
|
3705
|
-
return {
|
|
3706
|
-
commitSha: headCommit(repoDir),
|
|
3707
|
-
branch: STAGING_BRANCH,
|
|
3708
|
-
committed: hasMeaningfulChanges(repoDir) ? false : true,
|
|
3709
|
-
lockfileValidation: lockfileSafety.lockfileValidation,
|
|
3710
|
-
lockfileInstall: lockfileSafety.install
|
|
3711
|
-
};
|
|
3712
|
-
});
|
|
3713
|
-
rootRepo.merged = true;
|
|
3714
|
-
rootRepo.committed = true;
|
|
3715
|
-
rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
|
|
3716
|
-
rootRepo.pushed = true;
|
|
3717
|
-
rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3718
|
-
} catch (error) {
|
|
3719
|
-
const report = collectMergeConflictReport(repoDir);
|
|
3720
|
-
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
3721
|
-
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
3722
|
-
exitCode: 12
|
|
3723
|
-
});
|
|
3724
|
-
}
|
|
3725
|
-
const stageWorkflowGateResult = !waitForStaging ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForWorkflowGates("stage", [
|
|
3726
|
-
hostedDeployGate({
|
|
3727
|
-
name: rootRepo.name,
|
|
3728
|
-
repoPath: rootRepo.path,
|
|
3729
|
-
workflow: "deploy.yml",
|
|
3730
|
-
branch: STAGING_BRANCH,
|
|
3731
|
-
headSha: rootRepo.commitSha
|
|
3732
|
-
}),
|
|
3733
|
-
...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
|
|
3734
|
-
name: report.name,
|
|
3735
|
-
repoPath: report.path,
|
|
3736
|
-
workflow: "verify.yml",
|
|
3737
|
-
branch: STAGING_BRANCH,
|
|
3738
|
-
headSha: String(report.commitSha)
|
|
3739
|
-
}))
|
|
3740
|
-
], ciMode, {
|
|
3741
|
-
root,
|
|
3742
|
-
runId: workflowRun.runId,
|
|
3743
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3744
|
-
}).then((workflowGates) => ({
|
|
3745
|
-
status: "completed",
|
|
3746
|
-
workflowGates
|
|
3747
|
-
})));
|
|
3748
|
-
const stagingWait = {
|
|
3749
|
-
status: String(stageWorkflowGateResult?.status ?? "completed"),
|
|
3750
|
-
workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
|
|
3751
|
-
};
|
|
3752
|
-
const stageReleasePlan = buildReleasePlanSnapshot({
|
|
3753
|
-
root,
|
|
3754
|
-
mode,
|
|
3755
|
-
level: "patch",
|
|
3756
|
-
packageSelection: session.packageSelection,
|
|
3757
|
-
packageReports,
|
|
3758
|
-
rootRepo,
|
|
3759
|
-
blockers: []
|
|
4804
|
+
return { status: "completed", results };
|
|
3760
4805
|
});
|
|
3761
|
-
const
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
const
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
4806
|
+
const mergeChanged = Array.isArray(mergeDown?.results) && mergeDown.results.some((entry) => Boolean(entry.merged));
|
|
4807
|
+
const saveResult = mergeChanged || hasMeaningfulChanges(repoRoot(root)) ? await executeJournalStep(root, workflowRun.runId, "save-integrated-feature", () => workflowSave(helpersForCwd(helpers, root), {
|
|
4808
|
+
message: `integrate staging before stage: ${message}`,
|
|
4809
|
+
verifyMode: "skip",
|
|
4810
|
+
ciMode: "off",
|
|
4811
|
+
refreshPreview: false,
|
|
4812
|
+
preview: false,
|
|
4813
|
+
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
4814
|
+
})) : (skipJournalStep(root, workflowRun.runId, "save-integrated-feature", { skippedReason: "staging already integrated" }), null);
|
|
4815
|
+
const verification = verifyMode === "none" || process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE === "skip" ? (skipJournalStep(root, workflowRun.runId, "verify-integrated-feature", { mode: verifyMode, status: "skipped" }), {
|
|
4816
|
+
mode: verifyMode,
|
|
4817
|
+
status: "skipped",
|
|
4818
|
+
completedAt: null
|
|
4819
|
+
}) : await executeJournalStep(root, workflowRun.runId, "verify-integrated-feature", async () => {
|
|
4820
|
+
const proof = await runTreeseedProof({
|
|
4821
|
+
root,
|
|
4822
|
+
target: "staging",
|
|
4823
|
+
driver: verifyMode === "action" ? "act" : "local",
|
|
4824
|
+
write: (line, stream) => helpers.write(`[stage][verify] ${line}`, stream)
|
|
4825
|
+
});
|
|
4826
|
+
if (proof.failures.length > 0) {
|
|
4827
|
+
const first = proof.failures[0];
|
|
4828
|
+
workflowError("stage", "validation_failed", [
|
|
4829
|
+
"Treeseed stage proof failed.",
|
|
4830
|
+
`- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
|
|
4831
|
+
first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null
|
|
4832
|
+
].join("\n"), {
|
|
4833
|
+
details: { proof }
|
|
4834
|
+
});
|
|
3775
4835
|
}
|
|
3776
4836
|
return {
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
4837
|
+
mode: verifyMode,
|
|
4838
|
+
status: "passed",
|
|
4839
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4840
|
+
proof
|
|
3781
4841
|
};
|
|
3782
4842
|
});
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
const report = findReportByName(packageReports, pkg.name);
|
|
3790
|
-
if (!report) {
|
|
3791
|
-
continue;
|
|
3792
|
-
}
|
|
3793
|
-
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
3794
|
-
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
3795
|
-
targetBranch: STAGING_BRANCH
|
|
4843
|
+
const manifest = await executeJournalStep(root, workflowRun.runId, "write-stage-candidate", () => {
|
|
4844
|
+
const currentPlan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
|
|
4845
|
+
return writeStageCandidateManifest(root, workflowRun.runId, createStageCandidateManifest(root, workflowRun.runId, featureBranch, currentPlan, {
|
|
4846
|
+
mode: verifyMode,
|
|
4847
|
+
status: verification.status,
|
|
4848
|
+
completedAt: verification.completedAt
|
|
3796
4849
|
}));
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
4850
|
+
});
|
|
4851
|
+
const typedManifest = manifest;
|
|
4852
|
+
const promotion = await executeJournalStep(root, workflowRun.runId, "promote-to-staging", () => {
|
|
4853
|
+
const results = [];
|
|
4854
|
+
for (const pkg of typedManifest.packages) {
|
|
4855
|
+
results.push({
|
|
4856
|
+
name: pkg.name,
|
|
4857
|
+
...promoteCommitToBranchWithExpectedHead(resolve(root, pkg.path), {
|
|
4858
|
+
commitSha: pkg.commit,
|
|
4859
|
+
targetBranch: STAGING_BRANCH,
|
|
4860
|
+
expectedBefore: typedManifest.stagingHeadsBefore[pkg.name] ?? null
|
|
4861
|
+
})
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
results.push({
|
|
4865
|
+
name: "@treeseed/market",
|
|
4866
|
+
...promoteCommitToBranchWithExpectedHead(repoRoot(root), {
|
|
4867
|
+
commitSha: typedManifest.root.commit,
|
|
4868
|
+
targetBranch: STAGING_BRANCH,
|
|
4869
|
+
expectedBefore: typedManifest.stagingHeadsBefore["@treeseed/market"] ?? null
|
|
4870
|
+
})
|
|
4871
|
+
});
|
|
4872
|
+
return { status: "completed", results };
|
|
4873
|
+
});
|
|
4874
|
+
const stagingRefs = await executeJournalStep(root, workflowRun.runId, "verify-staging-refs", () => {
|
|
4875
|
+
const refs = {};
|
|
4876
|
+
for (const pkg of typedManifest.packages) {
|
|
4877
|
+
const observed = remoteHeadCommit(resolve(root, pkg.path), STAGING_BRANCH);
|
|
4878
|
+
if (observed !== pkg.commit) {
|
|
4879
|
+
throw new Error(`${pkg.name} staging ref mismatch: expected ${pkg.commit}, observed ${observed}.`);
|
|
4880
|
+
}
|
|
4881
|
+
refs[pkg.name] = observed;
|
|
4882
|
+
}
|
|
4883
|
+
const rootObserved = remoteHeadCommit(repoRoot(root), STAGING_BRANCH);
|
|
4884
|
+
if (rootObserved !== typedManifest.root.commit) {
|
|
4885
|
+
throw new Error(`@treeseed/market staging ref mismatch: expected ${typedManifest.root.commit}, observed ${rootObserved}.`);
|
|
4886
|
+
}
|
|
4887
|
+
refs["@treeseed/market"] = rootObserved;
|
|
4888
|
+
return { status: "verified", refs };
|
|
4889
|
+
});
|
|
4890
|
+
const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForStagingAutomation(repoRoot(root))) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
|
|
4891
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
4892
|
+
const cleanup = cleanupMode === "success" ? await executeJournalStep(root, workflowRun.runId, "cleanup-source", () => cleanupStageSourceBranches(root, featureBranch, typedManifest)) : (skipJournalStep(root, workflowRun.runId, "cleanup-source", { skippedReason: "manual cleanup selected" }), { status: "skipped", reason: "manual cleanup selected" });
|
|
3803
4893
|
const payload = {
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
releaseCandidate,
|
|
3816
|
-
previewCleanup,
|
|
3817
|
-
lockfileValidation: rootMerge?.lockfileValidation ?? null,
|
|
3818
|
-
lockfileInstall: rootMerge?.lockfileInstall ?? null,
|
|
3819
|
-
remoteDeleted: rootRepo.deletedRemote,
|
|
3820
|
-
localDeleted: rootRepo.deletedLocal,
|
|
3821
|
-
finalBranch,
|
|
4894
|
+
...basePayload,
|
|
4895
|
+
blockers: [],
|
|
4896
|
+
runId: workflowRun.runId,
|
|
4897
|
+
mergeDown,
|
|
4898
|
+
saveResult,
|
|
4899
|
+
verification,
|
|
4900
|
+
manifest: typedManifest,
|
|
4901
|
+
promotion,
|
|
4902
|
+
stagingRefs,
|
|
4903
|
+
hostedCi,
|
|
4904
|
+
cleanup,
|
|
3822
4905
|
workspaceLinks,
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
hostingAudit,
|
|
3826
|
-
worktreeCleanup,
|
|
3827
|
-
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3828
|
-
managedWorktree,
|
|
3829
|
-
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
3830
|
-
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
4906
|
+
finalBranch: cleanupMode === "success" ? STAGING_BRANCH : currentBranch(repoRoot(root)) || featureBranch,
|
|
4907
|
+
summary: "Stage promoted refs only. Hosted CI/CD was not waited on by default. Run `npx trsd ci --branch staging --json` or assign the staging release agent for CI/CD repair."
|
|
3831
4908
|
};
|
|
3832
4909
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
3833
|
-
return buildWorkflowResult(
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
{ operation: "status", reason: "Inspect staging readiness after the task branch merge." }
|
|
3842
|
-
])
|
|
3843
|
-
}
|
|
3844
|
-
);
|
|
4910
|
+
return buildWorkflowResult("stage", root, payload, {
|
|
4911
|
+
runId: workflowRun.runId,
|
|
4912
|
+
summary: "Treeseed stage completed successfully.",
|
|
4913
|
+
includeFinalState: false,
|
|
4914
|
+
nextSteps: createNextSteps([
|
|
4915
|
+
{ operation: "ci", reason: "Inspect staging CI/CD status after branch promotion.", input: { branch: STAGING_BRANCH, failed: true } }
|
|
4916
|
+
])
|
|
4917
|
+
});
|
|
3845
4918
|
} catch (error) {
|
|
3846
|
-
|
|
4919
|
+
try {
|
|
4920
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4921
|
+
} catch {
|
|
4922
|
+
}
|
|
3847
4923
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
3848
4924
|
resumable: true,
|
|
3849
4925
|
runId: workflowRun.runId,
|
|
@@ -3859,44 +4935,97 @@ async function workflowStage(helpers, input) {
|
|
|
3859
4935
|
toError("stage", error);
|
|
3860
4936
|
}
|
|
3861
4937
|
}
|
|
3862
|
-
async function
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4938
|
+
async function runReleaseGateReconcileFacade(operation, helpers, root, target, input, extraPayload = {}) {
|
|
4939
|
+
const executionMode = normalizeExecutionMode(input);
|
|
4940
|
+
const selector = {
|
|
4941
|
+
environment: target.kind === "persistent" ? target.scope : "staging",
|
|
4942
|
+
resourceKind: ["release-gate"],
|
|
4943
|
+
provider: ["treeseed"]
|
|
4944
|
+
};
|
|
4945
|
+
const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
|
|
4946
|
+
const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")));
|
|
4947
|
+
const rawUnitIds = new Set(rawUnits.map((unit) => unit.unitId));
|
|
4948
|
+
const units = rawUnits.map((unit) => ({
|
|
4949
|
+
...unit,
|
|
4950
|
+
dependencies: unit.dependencies.filter((dependency) => rawUnitIds.has(dependency))
|
|
4951
|
+
}));
|
|
4952
|
+
const unitSelector = {
|
|
4953
|
+
environment: selector.environment,
|
|
4954
|
+
unitId: units.map((unit) => unit.unitId)
|
|
4955
|
+
};
|
|
4956
|
+
const plan = await planTreeseedReconciliation({
|
|
4957
|
+
tenantRoot: root,
|
|
4958
|
+
target,
|
|
4959
|
+
env: helpers.context.env,
|
|
4960
|
+
units,
|
|
4961
|
+
selector: unitSelector,
|
|
4962
|
+
write: (line) => helpers.write(`[${operation}][reconcile] ${line}`, "stderr")
|
|
4963
|
+
});
|
|
4964
|
+
const blockers = Array.isArray(extraPayload.blockers) ? extraPayload.blockers.map((blocker) => String(blocker)).filter(Boolean) : [];
|
|
4965
|
+
if (executionMode === "execute" && blockers.length > 0) {
|
|
4966
|
+
workflowError(operation, "validation_failed", `${operation} is blocked:
|
|
4967
|
+
${blockers.join("\n")}`, {
|
|
4968
|
+
details: {
|
|
4969
|
+
blockers,
|
|
4970
|
+
target,
|
|
4971
|
+
plannedSteps: plan.plans.map((entry) => ({
|
|
4972
|
+
id: entry.unit.unitId,
|
|
4973
|
+
action: entry.diff.action,
|
|
4974
|
+
reasons: entry.diff.reasons
|
|
4975
|
+
}))
|
|
4976
|
+
}
|
|
3876
4977
|
});
|
|
3877
|
-
} catch (error) {
|
|
3878
|
-
toError("tags:cleanup", error);
|
|
3879
4978
|
}
|
|
4979
|
+
const result = executionMode === "execute" ? await reconcileTreeseedTarget({
|
|
4980
|
+
tenantRoot: root,
|
|
4981
|
+
target,
|
|
4982
|
+
env: helpers.context.env,
|
|
4983
|
+
units,
|
|
4984
|
+
selector: unitSelector,
|
|
4985
|
+
dryRun: input.execute !== true,
|
|
4986
|
+
write: (line) => helpers.write(`[${operation}][reconcile] ${line}`, "stderr")
|
|
4987
|
+
}) : null;
|
|
4988
|
+
const payload = {
|
|
4989
|
+
...extraPayload,
|
|
4990
|
+
mode: "reconcile-release-gates",
|
|
4991
|
+
target,
|
|
4992
|
+
executionMode,
|
|
4993
|
+
verifyDeployedResources: input.verifyDeployedResources === true,
|
|
4994
|
+
desiredGraph,
|
|
4995
|
+
units: units.map((unit) => ({
|
|
4996
|
+
unitId: unit.unitId,
|
|
4997
|
+
unitType: unit.unitType,
|
|
4998
|
+
provider: unit.provider,
|
|
4999
|
+
logicalName: unit.logicalName,
|
|
5000
|
+
dependencies: unit.dependencies
|
|
5001
|
+
})),
|
|
5002
|
+
plannedSteps: plan.plans.map((entry) => ({
|
|
5003
|
+
id: entry.unit.unitId,
|
|
5004
|
+
description: `${entry.unit.provider}:${entry.unit.unitType} ${entry.unit.logicalName}`,
|
|
5005
|
+
action: entry.diff.action,
|
|
5006
|
+
reasons: entry.diff.reasons
|
|
5007
|
+
})),
|
|
5008
|
+
reconcile: result,
|
|
5009
|
+
legacyMutationPathDisabled: true
|
|
5010
|
+
};
|
|
5011
|
+
return buildWorkflowResult(operation, root, payload, {
|
|
5012
|
+
executionMode,
|
|
5013
|
+
summary: executionMode === "execute" ? `${operation} release gates reconciled through the canonical adapter path.` : `${operation} release gate plan ready.`,
|
|
5014
|
+
nextSteps: createNextSteps([
|
|
5015
|
+
operation === "stage" ? { operation: "release", reason: "Promote production after the staging release-gate candidate is green.", input: { bump: "patch", plan: true } } : { operation: "status", reason: "Inspect production readiness after release gates complete." }
|
|
5016
|
+
])
|
|
5017
|
+
});
|
|
3880
5018
|
}
|
|
3881
5019
|
async function workflowRelease(helpers, input) {
|
|
3882
5020
|
try {
|
|
3883
5021
|
return await withContextEnv(helpers.context.env, async () => {
|
|
3884
|
-
const root = resolveProjectRootOrThrow("release", helpers.cwd());
|
|
3885
|
-
reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
|
|
3886
|
-
operation: "release",
|
|
3887
|
-
onProgress: (line, stream) => helpers.write(line, stream),
|
|
3888
|
-
throwOnBlocker: true
|
|
3889
|
-
});
|
|
5022
|
+
const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
|
|
3890
5023
|
const session = resolveTreeseedWorkflowSession(root);
|
|
3891
|
-
const gitRoot = session.gitRoot;
|
|
3892
|
-
const mode = session.mode;
|
|
3893
5024
|
const executionMode = normalizeExecutionMode(input);
|
|
3894
5025
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3895
5026
|
const packageReports = createWorkspacePackageReports(root);
|
|
3896
5027
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3897
|
-
const
|
|
3898
|
-
const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
3899
|
-
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: true }) : null;
|
|
5028
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
|
|
3900
5029
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3901
5030
|
const effectiveInput = autoResumeRun ? {
|
|
3902
5031
|
...autoResumeRun.input,
|
|
@@ -3904,11 +5033,10 @@ async function workflowRelease(helpers, input) {
|
|
|
3904
5033
|
} : input;
|
|
3905
5034
|
const level = effectiveInput.bump ?? "patch";
|
|
3906
5035
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
3907
|
-
const isResume = Boolean(explicitResumeRunId || autoResumeRun);
|
|
3908
5036
|
const packageSelection = session.packageSelection;
|
|
3909
5037
|
const plannedRelease = buildReleasePlanSnapshot({
|
|
3910
5038
|
root,
|
|
3911
|
-
mode,
|
|
5039
|
+
mode: session.mode,
|
|
3912
5040
|
level,
|
|
3913
5041
|
repairVersionLine: effectiveInput.repairVersionLine === true,
|
|
3914
5042
|
targetVersionLine: effectiveInput.targetVersionLine,
|
|
@@ -3917,70 +5045,91 @@ async function workflowRelease(helpers, input) {
|
|
|
3917
5045
|
rootRepo,
|
|
3918
5046
|
blockers: []
|
|
3919
5047
|
});
|
|
3920
|
-
const
|
|
3921
|
-
const
|
|
3922
|
-
const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, plannedPackageSelection.selected, {
|
|
5048
|
+
const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
|
|
5049
|
+
const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
3923
5050
|
level,
|
|
3924
5051
|
repairVersionLine: effectiveInput.repairVersionLine === true
|
|
3925
5052
|
});
|
|
5053
|
+
const plannedReadiness = collectTreeseedDeploymentReadiness({
|
|
5054
|
+
tenantRoot: root,
|
|
5055
|
+
environment: "prod",
|
|
5056
|
+
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
5057
|
+
});
|
|
5058
|
+
blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
3926
5059
|
plannedRelease.blockers = blockers;
|
|
5060
|
+
const releaseBasePayload = {
|
|
5061
|
+
...plannedRelease,
|
|
5062
|
+
readiness: plannedReadiness,
|
|
5063
|
+
ciMode,
|
|
5064
|
+
level,
|
|
5065
|
+
fresh: input.fresh === true,
|
|
5066
|
+
freshArchivedRuns: [],
|
|
5067
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
5068
|
+
runId: planAutoResumeRun.runId,
|
|
5069
|
+
branch: planAutoResumeRun.session.branchName,
|
|
5070
|
+
failure: planAutoResumeRun.failure
|
|
5071
|
+
} : null,
|
|
5072
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
5073
|
+
};
|
|
3927
5074
|
if (executionMode === "plan") {
|
|
3928
|
-
return
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
}, {
|
|
3940
|
-
executionMode,
|
|
3941
|
-
nextSteps: createNextSteps([
|
|
3942
|
-
{ operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
|
|
3943
|
-
])
|
|
3944
|
-
});
|
|
5075
|
+
return runReleaseGateReconcileFacade(
|
|
5076
|
+
"release",
|
|
5077
|
+
helpers,
|
|
5078
|
+
root,
|
|
5079
|
+
{ kind: "persistent", scope: "prod" },
|
|
5080
|
+
{
|
|
5081
|
+
plan: true,
|
|
5082
|
+
verifyDeployedResources: effectiveInput.verifyDeployedResources
|
|
5083
|
+
},
|
|
5084
|
+
releaseBasePayload
|
|
5085
|
+
);
|
|
3945
5086
|
}
|
|
3946
5087
|
if (blockers.length > 0) {
|
|
3947
|
-
workflowError("release", "validation_failed", blockers
|
|
3948
|
-
|
|
5088
|
+
workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
|
|
5089
|
+
${blockers.join("\n")}`, {
|
|
5090
|
+
details: { blockers, releasePlan: plannedRelease }
|
|
3949
5091
|
});
|
|
3950
5092
|
}
|
|
5093
|
+
const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
5094
|
+
const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
|
|
5095
|
+
const stableVersions = new Map([
|
|
5096
|
+
...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
|
|
5097
|
+
...selectedVersions.entries()
|
|
5098
|
+
]);
|
|
5099
|
+
const allVersions = new Map([
|
|
5100
|
+
["@treeseed/market", plannedRelease.rootVersion],
|
|
5101
|
+
...stableVersions.entries()
|
|
5102
|
+
]);
|
|
5103
|
+
const selectedPackageSet = new Set(selectedPackageNames);
|
|
3951
5104
|
const workflowRun = acquireWorkflowRun(
|
|
3952
5105
|
"release",
|
|
3953
5106
|
session,
|
|
3954
5107
|
{
|
|
5108
|
+
...effectiveInput,
|
|
3955
5109
|
bump: level,
|
|
3956
|
-
repairVersionLine: effectiveInput.repairVersionLine === true,
|
|
3957
|
-
targetVersionLine: effectiveInput.targetVersionLine,
|
|
3958
|
-
devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
|
|
3959
|
-
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3960
|
-
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3961
5110
|
ciMode,
|
|
3962
|
-
fresh: input.fresh === true
|
|
3963
|
-
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3964
|
-
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
5111
|
+
fresh: input.fresh === true
|
|
3965
5112
|
},
|
|
3966
5113
|
[
|
|
3967
|
-
{ id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3968
|
-
{ id: "release-
|
|
3969
|
-
{ id: "
|
|
3970
|
-
{ id: "
|
|
3971
|
-
...
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
{ id: "release-
|
|
3983
|
-
|
|
5114
|
+
{ id: "release-plan", description: "Record immutable release plan and target versions", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5115
|
+
{ id: "release-gates", description: "Run production release gates against staging evidence", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5116
|
+
{ id: "workspace-unlink", description: "Remove local workspace links before stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5117
|
+
{ id: "prepare-release-metadata", description: "Rewrite package metadata and lockfiles to production dependency mode", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5118
|
+
...selectedPackageNames.map((name) => {
|
|
5119
|
+
const report = packageReports.find((entry) => entry.name === name);
|
|
5120
|
+
return {
|
|
5121
|
+
id: `release-${name}`,
|
|
5122
|
+
description: `Release ${name} ${selectedVersions.get(name) ?? "(planned)"}`,
|
|
5123
|
+
repoName: name,
|
|
5124
|
+
repoPath: report?.path ?? root,
|
|
5125
|
+
branch: STAGING_BRANCH,
|
|
5126
|
+
resumable: true
|
|
5127
|
+
};
|
|
5128
|
+
}),
|
|
5129
|
+
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5130
|
+
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5131
|
+
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5132
|
+
{ id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
|
|
3984
5133
|
],
|
|
3985
5134
|
autoResumeRun ? {
|
|
3986
5135
|
...helpers.context,
|
|
@@ -3991,546 +5140,188 @@ async function workflowRelease(helpers, input) {
|
|
|
3991
5140
|
} : helpers.context
|
|
3992
5141
|
);
|
|
3993
5142
|
if (autoResumeRun) {
|
|
3994
|
-
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}
|
|
3995
|
-
}
|
|
3996
|
-
const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
|
|
3997
|
-
if (resumeAtRootGates) {
|
|
3998
|
-
helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
|
|
5143
|
+
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}.`);
|
|
3999
5144
|
}
|
|
4000
|
-
let releaseCleanupSnapshot = null;
|
|
4001
5145
|
try {
|
|
4002
|
-
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () =>
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
const
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
5146
|
+
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => ({
|
|
5147
|
+
...releaseBasePayload,
|
|
5148
|
+
freshArchivedRuns: freshPreparation.archived
|
|
5149
|
+
}));
|
|
5150
|
+
const releaseGates = await executeJournalStep(root, workflowRun.runId, "release-gates", async () => runReleaseGateReconcileFacade(
|
|
5151
|
+
"release",
|
|
5152
|
+
helpers,
|
|
5153
|
+
root,
|
|
5154
|
+
{ kind: "persistent", scope: "prod" },
|
|
5155
|
+
{
|
|
5156
|
+
execute: true,
|
|
5157
|
+
verifyDeployedResources: effectiveInput.verifyDeployedResources
|
|
5158
|
+
},
|
|
5159
|
+
{
|
|
5160
|
+
...releaseBasePayload,
|
|
5161
|
+
freshArchivedRuns: freshPreparation.archived
|
|
5162
|
+
}
|
|
5163
|
+
));
|
|
5164
|
+
const workspaceUnlink = await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
5165
|
+
const releaseMetadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
5166
|
+
applyStableWorkspaceVersionChanges(root, allVersions);
|
|
5167
|
+
const adapterMetadata = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => ({
|
|
4017
5168
|
name: pkg.name,
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
branch: STAGING_BRANCH,
|
|
4021
|
-
headSha: headCommit(pkg.dir)
|
|
5169
|
+
version: selectedVersions.get(pkg.name) ?? null,
|
|
5170
|
+
result: selectedVersions.has(pkg.name) ? prepareAdapterReleaseMetadata(root, pkg, selectedVersions.get(pkg.name)) : { status: "skipped", reason: "no planned version" }
|
|
4022
5171
|
}));
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
headSha: headCommit(gitRoot)
|
|
4030
|
-
}),
|
|
4031
|
-
...packageGates
|
|
4032
|
-
], ciMode, {
|
|
4033
|
-
root,
|
|
4034
|
-
runId: workflowRun.runId,
|
|
4035
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4036
|
-
}).then((workflowGates) => ({ workflowGates }));
|
|
4037
|
-
});
|
|
4038
|
-
const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
|
|
4039
|
-
if (!resumeAtRootGates && !isResume) {
|
|
4040
|
-
assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
|
|
4041
|
-
assertCleanWorktree(root);
|
|
4042
|
-
}
|
|
4043
|
-
if (!resumeAtRootGates) {
|
|
4044
|
-
prepareReleaseBranches(root);
|
|
4045
|
-
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4046
|
-
runWorkspaceReleasePreflight({ cwd: root });
|
|
4047
|
-
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
4048
|
-
}
|
|
4049
|
-
if (mode === "root-only") {
|
|
4050
|
-
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
4051
|
-
setRootPackageJsonVersion(root, rootVersion);
|
|
4052
|
-
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
4053
|
-
const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
|
|
4054
|
-
const changelog = updateReleaseChangelog(gitRoot, {
|
|
4055
|
-
version: rootVersion,
|
|
4056
|
-
commits: rootCommitsBeforeChangelog,
|
|
4057
|
-
extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
|
|
4058
|
-
});
|
|
4059
|
-
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4060
|
-
subject: `release: ${level} bump`,
|
|
4061
|
-
version: rootVersion,
|
|
4062
|
-
tagName: rootVersion,
|
|
4063
|
-
commits: rootCommitsBeforeChangelog,
|
|
4064
|
-
changelog
|
|
4065
|
-
}));
|
|
4066
|
-
pushBranch(gitRoot, STAGING_BRANCH);
|
|
4067
|
-
const stagingCommit = headCommit(gitRoot);
|
|
4068
|
-
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
4069
|
-
const released = mergeBranchIntoTarget(root, {
|
|
4070
|
-
sourceBranch: STAGING_BRANCH,
|
|
4071
|
-
targetBranch: PRODUCTION_BRANCH,
|
|
4072
|
-
message: releaseAdminMessage({
|
|
4073
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4074
|
-
version: rootVersion,
|
|
4075
|
-
tagName: rootVersion,
|
|
4076
|
-
commits: rootCommits,
|
|
4077
|
-
changelog
|
|
4078
|
-
}),
|
|
4079
|
-
pushTarget: true
|
|
4080
|
-
});
|
|
4081
|
-
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
|
|
4082
|
-
subject: `release: ${rootVersion}`,
|
|
4083
|
-
version: rootVersion,
|
|
4084
|
-
tagName: rootVersion,
|
|
4085
|
-
commits: rootCommits,
|
|
4086
|
-
changelog
|
|
4087
|
-
}));
|
|
4088
|
-
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
4089
|
-
return {
|
|
4090
|
-
rootVersion,
|
|
4091
|
-
stagingCommit,
|
|
4092
|
-
releasedCommit: released.commitSha,
|
|
4093
|
-
tag,
|
|
4094
|
-
changelog,
|
|
4095
|
-
adminCommitSummary: {
|
|
4096
|
-
commitCount: rootCommits.length,
|
|
4097
|
-
notableCommits: rootCommits.slice(0, 12)
|
|
4098
|
-
}
|
|
4099
|
-
};
|
|
4100
|
-
});
|
|
4101
|
-
rootRepo.committed = true;
|
|
4102
|
-
rootRepo.pushed = true;
|
|
4103
|
-
rootRepo.merged = true;
|
|
4104
|
-
rootRepo.branch = PRODUCTION_BRANCH;
|
|
4105
|
-
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
4106
|
-
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
4107
|
-
const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
4108
|
-
hostedDeployGate({
|
|
4109
|
-
name: rootRepo.name,
|
|
4110
|
-
repoPath: rootRepo.path,
|
|
4111
|
-
workflow: "deploy.yml",
|
|
4112
|
-
branch: rootVersion,
|
|
4113
|
-
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
4114
|
-
})
|
|
4115
|
-
].filter((gate) => gate.headSha), ciMode, {
|
|
4116
|
-
root,
|
|
4117
|
-
runId: workflowRun.runId,
|
|
4118
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4119
|
-
}).then((workflowGates) => ({ workflowGates })));
|
|
4120
|
-
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
4121
|
-
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4122
|
-
const hostingAudit2 = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
|
|
4123
|
-
enabled: true,
|
|
4124
|
-
strict: false
|
|
4125
|
-
});
|
|
4126
|
-
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
|
|
4127
|
-
version: rootVersion,
|
|
4128
|
-
changelog: rootRelease2?.changelog ?? null
|
|
4129
|
-
}));
|
|
4130
|
-
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4131
|
-
const payload2 = {
|
|
4132
|
-
mode,
|
|
4133
|
-
mergeStrategy: "merge-commit",
|
|
4134
|
-
level,
|
|
4135
|
-
fresh: input.fresh === true,
|
|
4136
|
-
freshArchivedRuns: freshPreparation.archived,
|
|
4137
|
-
resumed: workflowRun.resumed,
|
|
4138
|
-
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
4139
|
-
autoResumed: autoResumeRun != null,
|
|
4140
|
-
rootVersion: String(rootRelease2?.rootVersion ?? ""),
|
|
4141
|
-
releaseTag: String(rootRelease2?.rootVersion ?? ""),
|
|
4142
|
-
releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
|
|
4143
|
-
stagingBranch: STAGING_BRANCH,
|
|
4144
|
-
productionBranch: PRODUCTION_BRANCH,
|
|
4145
|
-
touchedPackages: [],
|
|
4146
|
-
packageSelection: { changed: [], dependents: [], selected: [] },
|
|
4147
|
-
publishWait: [],
|
|
4148
|
-
repos: [],
|
|
4149
|
-
rootRepo,
|
|
4150
|
-
releaseCandidate,
|
|
4151
|
-
stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
|
|
4152
|
-
releaseBackMerge: releaseBackMerge2,
|
|
4153
|
-
hostedDeploymentState: hostedDeploymentState2,
|
|
4154
|
-
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
4155
|
-
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
4156
|
-
workspaceLinks: workspaceLinks2,
|
|
4157
|
-
ciMode,
|
|
4158
|
-
workflowGates: [
|
|
4159
|
-
...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
|
|
4160
|
-
...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
|
|
4161
|
-
],
|
|
4162
|
-
hostingAudit: hostingAudit2,
|
|
4163
|
-
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
4164
|
-
};
|
|
4165
|
-
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
4166
|
-
return buildWorkflowResult("release", root, payload2, {
|
|
4167
|
-
runId: workflowRun.runId,
|
|
4168
|
-
nextSteps: createNextSteps([
|
|
4169
|
-
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
4170
|
-
])
|
|
4171
|
-
});
|
|
4172
|
-
}
|
|
4173
|
-
if (!resumeAtRootGates) {
|
|
4174
|
-
validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
|
|
4175
|
-
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
4176
|
-
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4177
|
-
prepareReleaseBranches(pkg.dir);
|
|
4178
|
-
}
|
|
5172
|
+
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
5173
|
+
const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet).filter((issue) => issue.reason !== "git-release-ref" && issue.reason !== "lockfile-git-release-ref");
|
|
5174
|
+
if (remainingDevReferences.length > 0) {
|
|
5175
|
+
const rendered = remainingDevReferences.map((issue) => `${issue.repoName}: ${issue.filePath} ${issue.dependencyName ?? ""} ${issue.reason} ${issue.spec}`).join("\n");
|
|
5176
|
+
throw new Error(`Stable release metadata still contains development references.
|
|
5177
|
+
${rendered}`);
|
|
4179
5178
|
}
|
|
4180
|
-
}
|
|
4181
|
-
releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
4182
|
-
const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
4183
|
-
const releasedPackageDevTags2 = Object.fromEntries(
|
|
4184
|
-
checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
|
|
4185
|
-
const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
|
|
4186
|
-
return [pkg.name, String(packageJson.version ?? "")];
|
|
4187
|
-
}).filter(([, version]) => version.includes("-dev."))
|
|
4188
|
-
);
|
|
4189
|
-
const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveDependencyReplacementVersions);
|
|
4190
|
-
applyStableWorkspaceVersionChanges(root, effectiveVersions);
|
|
4191
|
-
setRootPackageJsonVersion(root, rootVersion);
|
|
4192
|
-
const releaseInstalls2 = [
|
|
4193
|
-
{ name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
|
|
4194
|
-
];
|
|
4195
|
-
assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
|
|
4196
5179
|
return {
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
5180
|
+
versions: Object.fromEntries(allVersions.entries()),
|
|
5181
|
+
adapterMetadata,
|
|
5182
|
+
rootInstall,
|
|
5183
|
+
workspaceUnlink
|
|
4200
5184
|
};
|
|
4201
|
-
}
|
|
4202
|
-
const
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
const report = findReportByName(packageReports, pkg.name);
|
|
4208
|
-
if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4209
|
-
if (report) {
|
|
4210
|
-
report.skippedReason = "unchanged";
|
|
4211
|
-
}
|
|
4212
|
-
continue;
|
|
4213
|
-
}
|
|
4214
|
-
const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
|
|
4215
|
-
checkoutBranch(pkg.dir, STAGING_BRANCH);
|
|
4216
|
-
const tagName = String(effectiveVersions.get(pkg.name));
|
|
4217
|
-
releaseInstalls.push({
|
|
4218
|
-
name: pkg.name,
|
|
4219
|
-
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
4220
|
-
});
|
|
4221
|
-
assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
|
|
4222
|
-
const packageCommitsBeforeChangelog = releaseHistoryCommits(pkg.dir);
|
|
5185
|
+
});
|
|
5186
|
+
const packageReleases = [];
|
|
5187
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root).filter((entry) => selectedPackageSet.has(entry.name))) {
|
|
5188
|
+
const version = selectedVersions.get(pkg.name);
|
|
5189
|
+
if (!version) continue;
|
|
5190
|
+
const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, () => {
|
|
4223
5191
|
const changelog = updateReleaseChangelog(pkg.dir, {
|
|
4224
|
-
version
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
});
|
|
4228
|
-
if (hasMeaningfulChanges(pkg.dir)) {
|
|
4229
|
-
run("git", ["add", "-A"], { cwd: pkg.dir });
|
|
4230
|
-
run("git", ["commit", "-m", releaseAdminMessage({
|
|
4231
|
-
subject: `release: ${tagName}`,
|
|
4232
|
-
version: tagName,
|
|
4233
|
-
tagName,
|
|
4234
|
-
commits: packageCommitsBeforeChangelog,
|
|
4235
|
-
changelog,
|
|
4236
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4237
|
-
})], { cwd: pkg.dir });
|
|
4238
|
-
}
|
|
4239
|
-
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
4240
|
-
const packageCommits = releaseHistoryCommits(pkg.dir);
|
|
4241
|
-
const mergeResult = mergeBranchIntoTarget(pkg.dir, {
|
|
4242
|
-
sourceBranch: STAGING_BRANCH,
|
|
4243
|
-
targetBranch: PRODUCTION_BRANCH,
|
|
4244
|
-
message: releaseAdminMessage({
|
|
4245
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4246
|
-
version: tagName,
|
|
4247
|
-
tagName,
|
|
4248
|
-
commits: packageCommits,
|
|
4249
|
-
changelog,
|
|
4250
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4251
|
-
}),
|
|
4252
|
-
pushTarget: true
|
|
4253
|
-
});
|
|
4254
|
-
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
|
|
4255
|
-
subject: `release: ${tagName}`,
|
|
4256
|
-
version: tagName,
|
|
4257
|
-
tagName,
|
|
4258
|
-
commits: packageCommits,
|
|
4259
|
-
changelog,
|
|
4260
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4261
|
-
}));
|
|
4262
|
-
const workflowGates = await waitForWorkflowGates("release", [
|
|
4263
|
-
{
|
|
4264
|
-
name: pkg.name,
|
|
4265
|
-
repoPath: pkg.dir,
|
|
4266
|
-
workflow: "publish.yml",
|
|
4267
|
-
headSha: mergeResult.commitSha,
|
|
4268
|
-
branch: tagName
|
|
4269
|
-
}
|
|
4270
|
-
], ciMode, {
|
|
4271
|
-
root,
|
|
4272
|
-
runId: workflowRun.runId,
|
|
4273
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
5192
|
+
version,
|
|
5193
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
5194
|
+
targetRef: "HEAD"
|
|
4274
5195
|
});
|
|
4275
|
-
const
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
targetRef: STAGING_BRANCH,
|
|
4283
|
-
commits: packageCommits,
|
|
4284
|
-
changelog,
|
|
4285
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
5196
|
+
const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
|
|
5197
|
+
subject: `release: ${pkg.name} ${version}`,
|
|
5198
|
+
version,
|
|
5199
|
+
tagName: version,
|
|
5200
|
+
sourceRef: STAGING_BRANCH,
|
|
5201
|
+
targetRef: PRODUCTION_BRANCH,
|
|
5202
|
+
changelog
|
|
4286
5203
|
}));
|
|
4287
|
-
|
|
5204
|
+
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
5205
|
+
const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
|
|
5206
|
+
const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
|
|
4288
5207
|
return {
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
5208
|
+
name: pkg.name,
|
|
5209
|
+
path: relative(root, pkg.dir),
|
|
5210
|
+
version,
|
|
4292
5211
|
changelog,
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
},
|
|
4297
|
-
publish,
|
|
4298
|
-
workflowGates,
|
|
4299
|
-
backMerge
|
|
5212
|
+
commit,
|
|
5213
|
+
promotion,
|
|
5214
|
+
tag
|
|
4300
5215
|
};
|
|
4301
5216
|
});
|
|
4302
|
-
|
|
4303
|
-
report.pushed = true;
|
|
4304
|
-
report.merged = true;
|
|
4305
|
-
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
4306
|
-
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
4307
|
-
report.publishWait = releasedPackage?.publish ?? null;
|
|
4308
|
-
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
4309
|
-
report.backMerge = releasedPackage?.backMerge ?? null;
|
|
4310
|
-
report.changelog = releasedPackage?.changelog ?? null;
|
|
4311
|
-
report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
|
|
4312
|
-
report.branch = STAGING_BRANCH;
|
|
4313
|
-
publishWait.push({
|
|
4314
|
-
name: report.name,
|
|
4315
|
-
...releasedPackage?.publish ?? {}
|
|
4316
|
-
});
|
|
5217
|
+
packageReleases.push(packageRelease);
|
|
4317
5218
|
}
|
|
4318
|
-
assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
|
|
4319
5219
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
commits: rootCommitsBeforeChangelog,
|
|
4326
|
-
extraDependencyBullets: [
|
|
4327
|
-
`Release @treeseed/market ${rootVersion}.`,
|
|
4328
|
-
...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
|
|
4329
|
-
]
|
|
5220
|
+
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
5221
|
+
version: plannedRelease.rootVersion,
|
|
5222
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
5223
|
+
targetRef: "HEAD",
|
|
5224
|
+
extraDependencyBullets: versionLines(stableVersions)
|
|
4330
5225
|
});
|
|
4331
|
-
commitAllIfChanged(
|
|
4332
|
-
subject: `release: ${
|
|
4333
|
-
version: rootVersion,
|
|
4334
|
-
tagName:
|
|
4335
|
-
|
|
4336
|
-
changelog,
|
|
4337
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
4338
|
-
}));
|
|
4339
|
-
pushBranch(gitRoot, STAGING_BRANCH);
|
|
4340
|
-
const stagingCommit = headCommit(gitRoot);
|
|
4341
|
-
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
4342
|
-
let released;
|
|
4343
|
-
let submoduleReconciliation = null;
|
|
4344
|
-
const mergeMessage = releaseAdminMessage({
|
|
4345
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4346
|
-
version: rootVersion,
|
|
4347
|
-
tagName: rootVersion,
|
|
4348
|
-
commits: rootCommits,
|
|
4349
|
-
changelog,
|
|
4350
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
4351
|
-
});
|
|
4352
|
-
try {
|
|
4353
|
-
released = mergeBranchIntoTarget(root, {
|
|
4354
|
-
sourceBranch: STAGING_BRANCH,
|
|
4355
|
-
targetBranch: PRODUCTION_BRANCH,
|
|
4356
|
-
message: mergeMessage,
|
|
4357
|
-
pushTarget: false,
|
|
4358
|
-
quietMerge: true
|
|
4359
|
-
});
|
|
4360
|
-
} catch (error) {
|
|
4361
|
-
const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
|
|
4362
|
-
if (!reconciliation.resolved) {
|
|
4363
|
-
throw error;
|
|
4364
|
-
}
|
|
4365
|
-
helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
|
|
4366
|
-
submoduleReconciliation = reconciliation;
|
|
4367
|
-
commitAllIfChanged(gitRoot, mergeMessage);
|
|
4368
|
-
released = { commitSha: headCommit(gitRoot) };
|
|
4369
|
-
}
|
|
4370
|
-
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
4371
|
-
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4372
|
-
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
4373
|
-
}
|
|
4374
|
-
}
|
|
4375
|
-
const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
|
|
4376
|
-
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4377
|
-
subject: "release: sync package main heads",
|
|
4378
|
-
version: rootVersion,
|
|
4379
|
-
tagName: rootVersion,
|
|
4380
|
-
sourceRef: "package main heads",
|
|
5226
|
+
const commit = commitAllIfChanged(repoRoot(root), releaseAdminMessage({
|
|
5227
|
+
subject: `release: market ${plannedRelease.rootVersion}`,
|
|
5228
|
+
version: plannedRelease.rootVersion,
|
|
5229
|
+
tagName: plannedRelease.releaseTag,
|
|
5230
|
+
sourceRef: STAGING_BRANCH,
|
|
4381
5231
|
targetRef: PRODUCTION_BRANCH,
|
|
4382
|
-
commits: mainPointerCommits,
|
|
4383
5232
|
changelog,
|
|
4384
|
-
extraLines: versionLines(
|
|
5233
|
+
extraLines: versionLines(stableVersions).map((line) => `Released package ${line}`)
|
|
4385
5234
|
}));
|
|
4386
|
-
|
|
4387
|
-
const
|
|
4388
|
-
|
|
4389
|
-
version: rootVersion,
|
|
4390
|
-
tagName: rootVersion,
|
|
4391
|
-
commits: rootCommits,
|
|
4392
|
-
changelog,
|
|
4393
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
4394
|
-
}));
|
|
4395
|
-
run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
|
|
4396
|
-
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
4397
|
-
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
5235
|
+
pushBranch(repoRoot(root), STAGING_BRANCH);
|
|
5236
|
+
const promotion = promoteCommitToProductionBranch(repoRoot(root), commit.commitSha);
|
|
5237
|
+
const tag = ensureReleaseTag(repoRoot(root), plannedRelease.releaseTag, commit.commitSha, `release: market ${plannedRelease.rootVersion}`);
|
|
4398
5238
|
return {
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
mergeCommit: released.commitSha,
|
|
4403
|
-
tag,
|
|
5239
|
+
name: "@treeseed/market",
|
|
5240
|
+
version: plannedRelease.rootVersion,
|
|
5241
|
+
releaseTag: plannedRelease.releaseTag,
|
|
4404
5242
|
changelog,
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
},
|
|
4409
|
-
submoduleReconciliation
|
|
5243
|
+
commit,
|
|
5244
|
+
promotion,
|
|
5245
|
+
tag
|
|
4410
5246
|
};
|
|
4411
5247
|
});
|
|
4412
|
-
|
|
4413
|
-
rootRepo.pushed = true;
|
|
4414
|
-
rootRepo.merged = true;
|
|
4415
|
-
rootRepo.branch = PRODUCTION_BRANCH;
|
|
4416
|
-
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
4417
|
-
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
4418
|
-
const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
5248
|
+
const publishGates = [
|
|
4419
5249
|
hostedDeployGate({
|
|
4420
|
-
name:
|
|
4421
|
-
repoPath:
|
|
4422
|
-
workflow: "deploy.yml",
|
|
4423
|
-
branch:
|
|
4424
|
-
headSha: String(rootRelease
|
|
4425
|
-
|
|
4426
|
-
|
|
5250
|
+
name: "@treeseed/market",
|
|
5251
|
+
repoPath: repoRoot(root),
|
|
5252
|
+
workflow: "deploy-web.yml",
|
|
5253
|
+
branch: PRODUCTION_BRANCH,
|
|
5254
|
+
headSha: String(rootRelease.commit.commitSha ?? ""),
|
|
5255
|
+
dispatchIfMissing: true,
|
|
5256
|
+
dispatchInputs: {
|
|
5257
|
+
environment: "prod",
|
|
5258
|
+
action_kind: "deploy_web"
|
|
5259
|
+
}
|
|
5260
|
+
}),
|
|
5261
|
+
...packageReleases.map((entry) => ({
|
|
5262
|
+
name: String(entry.name),
|
|
5263
|
+
repoPath: resolve(root, String(entry.path)),
|
|
5264
|
+
workflow: releaseWorkflowForPackage(root, String(entry.name)),
|
|
5265
|
+
branch: String(entry.version),
|
|
5266
|
+
headSha: String(entry.commit.commitSha ?? "")
|
|
5267
|
+
}))
|
|
5268
|
+
].filter((gate) => gate.headSha);
|
|
5269
|
+
const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
|
|
4427
5270
|
root,
|
|
4428
5271
|
runId: workflowRun.runId,
|
|
4429
5272
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4430
5273
|
}).then((workflowGates) => ({ workflowGates })));
|
|
4431
|
-
const
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
|
|
4444
|
-
const cleanup = collectStaleDevTagCleanupReports(root, {
|
|
4445
|
-
includePackages: effectivePackageSelection.selected,
|
|
4446
|
-
branchScope: "all"
|
|
4447
|
-
}, { execute: true });
|
|
4448
|
-
return { ...cleanup, replacedDevReferenceCount: replacedDevReferences.length, releasedPackageDevTagCount: releasedPackageDevTags.size };
|
|
5274
|
+
const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
|
|
5275
|
+
const packageBackMerges = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
|
|
5276
|
+
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
5277
|
+
version: selectedVersions.get(pkg.name) ?? null,
|
|
5278
|
+
sourceRef: PRODUCTION_BRANCH,
|
|
5279
|
+
targetRef: STAGING_BRANCH
|
|
5280
|
+
})));
|
|
5281
|
+
const rootBackMerge = backMergeRootProductionIntoStaging(root, true, {
|
|
5282
|
+
version: plannedRelease.rootVersion,
|
|
5283
|
+
selectedVersions: stableVersions
|
|
5284
|
+
});
|
|
5285
|
+
return { packages: packageBackMerges, root: rootBackMerge };
|
|
4449
5286
|
});
|
|
4450
|
-
|
|
4451
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
5287
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
4452
5288
|
const payload = {
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
stagingBranch: STAGING_BRANCH,
|
|
4465
|
-
productionBranch: PRODUCTION_BRANCH,
|
|
4466
|
-
touchedPackages: effectivePackageSelection.selected,
|
|
4467
|
-
packageSelection: effectivePackageSelection,
|
|
4468
|
-
replacedDevReferences,
|
|
4469
|
-
releaseInstalls,
|
|
4470
|
-
devTagCleanup,
|
|
4471
|
-
publishWait,
|
|
4472
|
-
repos: packageReports,
|
|
4473
|
-
rootRepo,
|
|
4474
|
-
releaseCandidate,
|
|
4475
|
-
stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
|
|
4476
|
-
releaseBackMerge,
|
|
4477
|
-
hostedDeploymentState,
|
|
4478
|
-
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
4479
|
-
pushStatus: {
|
|
4480
|
-
stagingPushed: true,
|
|
4481
|
-
productionPushed: true,
|
|
4482
|
-
tagPushed: true
|
|
4483
|
-
},
|
|
5289
|
+
...releaseBasePayload,
|
|
5290
|
+
mode: plannedRelease.mode,
|
|
5291
|
+
runId: workflowRun.runId,
|
|
5292
|
+
releasePlan,
|
|
5293
|
+
releaseGates,
|
|
5294
|
+
workspaceUnlink,
|
|
5295
|
+
releaseMetadata,
|
|
5296
|
+
packageReleases,
|
|
5297
|
+
rootRelease,
|
|
5298
|
+
publishWait: publishWait.workflowGates,
|
|
5299
|
+
backMerge,
|
|
4484
5300
|
workspaceLinks,
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
...packageReports.flatMap((report) => report.workflowGates),
|
|
4489
|
-
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
4490
|
-
],
|
|
4491
|
-
hostingAudit,
|
|
4492
|
-
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
5301
|
+
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|
|
5302
|
+
touchedPackages: selectedPackageNames,
|
|
5303
|
+
finalBranch: STAGING_BRANCH
|
|
4493
5304
|
};
|
|
4494
5305
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
4495
5306
|
return buildWorkflowResult("release", root, payload, {
|
|
4496
5307
|
runId: workflowRun.runId,
|
|
5308
|
+
summary: "Treeseed production release completed successfully.",
|
|
4497
5309
|
nextSteps: createNextSteps([
|
|
4498
|
-
{ operation: "status", reason: "Inspect release
|
|
5310
|
+
{ operation: "status", reason: "Inspect release state after production promotion." }
|
|
4499
5311
|
])
|
|
4500
5312
|
});
|
|
4501
5313
|
} catch (error) {
|
|
4502
|
-
const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
|
|
4503
|
-
const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
|
|
4504
|
-
const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
|
|
4505
|
-
const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
|
|
4506
|
-
helpers.write(`[release][recovery] Last release phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`, "stderr");
|
|
4507
5314
|
try {
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
});
|
|
4511
|
-
if (repair.blockers.length > 0) {
|
|
4512
|
-
helpers.write(`[release][recovery] Package repos need manual review before retrying:
|
|
4513
|
-
${repair.blockers.join("\n")}`, "stderr");
|
|
4514
|
-
}
|
|
4515
|
-
} catch (repairError) {
|
|
4516
|
-
helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
|
|
4517
|
-
}
|
|
4518
|
-
if (localCleanup.restored.length > 0) {
|
|
4519
|
-
helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
|
|
4520
|
-
}
|
|
4521
|
-
if (localCleanup.manualReview.length > 0) {
|
|
4522
|
-
helpers.write(`[release][recovery] Local cleanup needs manual review:
|
|
4523
|
-
${localCleanup.manualReview.map((entry) => `- ${String(entry.repo ?? entry.scope ?? "repo")}: ${String(entry.reason ?? "unknown")}`).join("\n")}`, "stderr");
|
|
5315
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
5316
|
+
} catch {
|
|
4524
5317
|
}
|
|
4525
|
-
helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
|
|
4526
5318
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
4527
5319
|
resumable: true,
|
|
4528
5320
|
runId: workflowRun.runId,
|
|
4529
5321
|
command: "release",
|
|
4530
|
-
message:
|
|
4531
|
-
recoverCommand: "
|
|
4532
|
-
resumeCommand: `
|
|
4533
|
-
localCleanup
|
|
5322
|
+
message: "Resume the interrupted production release after fixing the cause.",
|
|
5323
|
+
recoverCommand: "treeseed recover",
|
|
5324
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
4534
5325
|
});
|
|
4535
5326
|
throw error;
|
|
4536
5327
|
}
|
|
@@ -4592,6 +5383,8 @@ async function workflowResume(helpers, input) {
|
|
|
4592
5383
|
return workflowSwitch(resumedHelpers, journal.input);
|
|
4593
5384
|
case "save":
|
|
4594
5385
|
return workflowSave(resumedHelpers, journal.input);
|
|
5386
|
+
case "update":
|
|
5387
|
+
return workflowUpdate(resumedHelpers, journal.input);
|
|
4595
5388
|
case "close":
|
|
4596
5389
|
return workflowClose(resumedHelpers, journal.input);
|
|
4597
5390
|
case "stage":
|
|
@@ -4614,7 +5407,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
4614
5407
|
try {
|
|
4615
5408
|
return await withContextEnv(helpers.context.env, async () => {
|
|
4616
5409
|
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
4617
|
-
const lock = inspectWorkflowLock(root);
|
|
5410
|
+
const lock = inspectWorkflowLock(root, { scope: "worktree" });
|
|
4618
5411
|
const journals = listWorkflowRunJournals(root);
|
|
4619
5412
|
const session = resolveTreeseedWorkflowSession(root);
|
|
4620
5413
|
const currentHeads = Object.fromEntries(
|
|
@@ -4720,6 +5513,7 @@ async function workflowDestroy(helpers, input) {
|
|
|
4720
5513
|
const dryRun = executionMode === "plan";
|
|
4721
5514
|
const force = input.force === true;
|
|
4722
5515
|
const deleteData = input.deleteData === true;
|
|
5516
|
+
const sweepTreeseed = input.sweepTreeseed === true;
|
|
4723
5517
|
const destroyRemote = input.destroyRemote !== false;
|
|
4724
5518
|
const destroyLocal = input.destroyLocal !== false;
|
|
4725
5519
|
const removeBuildArtifacts = input.removeBuildArtifacts === true;
|
|
@@ -4733,6 +5527,7 @@ async function workflowDestroy(helpers, input) {
|
|
|
4733
5527
|
dryRun,
|
|
4734
5528
|
force,
|
|
4735
5529
|
deleteData,
|
|
5530
|
+
sweepTreeseed,
|
|
4736
5531
|
destroyRemote,
|
|
4737
5532
|
destroyLocal,
|
|
4738
5533
|
removeBuildArtifacts,
|
|
@@ -4743,12 +5538,13 @@ async function workflowDestroy(helpers, input) {
|
|
|
4743
5538
|
},
|
|
4744
5539
|
plannedSteps: [
|
|
4745
5540
|
...destroyRemote ? [{ id: "destroy-remote", description: `Destroy remote ${scope} resources` }] : [],
|
|
5541
|
+
...sweepTreeseed ? [{ id: "sweep-treeseed-resources", description: "Sweep TreeSeed-owned provider resources across persistent environments" }] : [],
|
|
4746
5542
|
...destroyLocal ? [{ id: "cleanup-local", description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}` }] : []
|
|
4747
5543
|
],
|
|
4748
5544
|
remoteResult: null
|
|
4749
5545
|
};
|
|
4750
5546
|
if (executionMode === "plan") {
|
|
4751
|
-
const plannedRemoteResult = destroyRemote ? await destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: true, force, deleteData, target }) : null;
|
|
5547
|
+
const plannedRemoteResult = destroyRemote ? await destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: true, force, deleteData, sweepTreeseed, target }) : null;
|
|
4752
5548
|
return buildWorkflowResult(
|
|
4753
5549
|
"destroy",
|
|
4754
5550
|
tenantRoot,
|
|
@@ -4759,7 +5555,7 @@ async function workflowDestroy(helpers, input) {
|
|
|
4759
5555
|
{
|
|
4760
5556
|
executionMode,
|
|
4761
5557
|
nextSteps: createNextSteps([
|
|
4762
|
-
{ operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, deleteData, removeBuildArtifacts } },
|
|
5558
|
+
{ operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, deleteData, sweepTreeseed, removeBuildArtifacts } },
|
|
4763
5559
|
{ operation: "status", reason: "Confirm the current environment state before making destructive changes." }
|
|
4764
5560
|
])
|
|
4765
5561
|
}
|
|
@@ -4772,6 +5568,7 @@ async function workflowDestroy(helpers, input) {
|
|
|
4772
5568
|
environment: scope,
|
|
4773
5569
|
force,
|
|
4774
5570
|
deleteData,
|
|
5571
|
+
sweepTreeseed,
|
|
4775
5572
|
destroyRemote,
|
|
4776
5573
|
destroyLocal,
|
|
4777
5574
|
removeBuildArtifacts
|
|
@@ -4801,7 +5598,7 @@ async function workflowDestroy(helpers, input) {
|
|
|
4801
5598
|
if (!confirmed) {
|
|
4802
5599
|
workflowError("destroy", "confirmation_required", `Destroy confirmation required. Re-run with confirm="${expectedConfirmation}".`);
|
|
4803
5600
|
}
|
|
4804
|
-
const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: false, force, deleteData, target })) : null;
|
|
5601
|
+
const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: false, force, deleteData, sweepTreeseed, target })) : null;
|
|
4805
5602
|
if (!destroyRemote) {
|
|
4806
5603
|
skipJournalStep(root, workflowRun.runId, "destroy-remote", { skippedReason: "destroyRemote=false" });
|
|
4807
5604
|
}
|
|
@@ -4851,19 +5648,27 @@ async function workflowDestroy(helpers, input) {
|
|
|
4851
5648
|
}
|
|
4852
5649
|
export {
|
|
4853
5650
|
TreeseedWorkflowError,
|
|
5651
|
+
destroyTreeseedBranchPreview,
|
|
5652
|
+
normalizeReleaseCandidateMode,
|
|
5653
|
+
normalizeSaveCiMode,
|
|
5654
|
+
normalizeSaveLane,
|
|
5655
|
+
reconcileTreeseedBranchPreview,
|
|
5656
|
+
shouldUseHostedSaveCi,
|
|
4854
5657
|
workflowCi,
|
|
4855
5658
|
workflowClose,
|
|
4856
5659
|
workflowConfig,
|
|
4857
5660
|
workflowDestroy,
|
|
4858
5661
|
workflowDev,
|
|
4859
5662
|
workflowExport,
|
|
5663
|
+
workflowProof,
|
|
4860
5664
|
workflowRecover,
|
|
4861
5665
|
workflowRelease,
|
|
5666
|
+
workflowReleaseCandidate,
|
|
4862
5667
|
workflowResume,
|
|
4863
5668
|
workflowSave,
|
|
4864
5669
|
workflowStage,
|
|
4865
5670
|
workflowStatus,
|
|
4866
5671
|
workflowSwitch,
|
|
4867
|
-
|
|
4868
|
-
|
|
5672
|
+
workflowTasks,
|
|
5673
|
+
workflowUpdate
|
|
4869
5674
|
};
|