@treeseed/sdk 0.11.0 → 0.12.5
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 +9 -4
- 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 +117 -60
- package/dist/capacity-provider.js +215 -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 +2 -2
- 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 +49037 -11843
- package/dist/db/market-schema.js +1788 -50
- package/dist/db/node-sqlite.d.ts +1 -232
- 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 +1 -1
- package/dist/hosting/builtins.d.ts +1 -1
- package/dist/hosting/builtins.js +9 -198
- package/dist/hosting/contracts.d.ts +3 -2
- package/dist/hosting/graph.d.ts +24 -21
- package/dist/hosting/graph.js +280 -294
- package/dist/hosting/index.d.ts +4 -4
- package/dist/index.d.ts +88 -74
- package/dist/index.js +81 -10
- 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 -8
- package/dist/market-client.d.ts +265 -7
- package/dist/market-client.js +381 -9
- 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 +1 -1
- 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 +73 -19
- package/dist/operations/services/config-runtime.js +276 -100
- package/dist/operations/services/deploy.d.ts +41 -622
- package/dist/operations/services/deploy.js +181 -213
- package/dist/operations/services/deployment-readiness.d.ts +1 -1
- package/dist/operations/services/deployment-readiness.js +38 -7
- 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 +43 -5
- package/dist/operations/services/git-workflow.js +242 -20
- package/dist/operations/services/github-actions-verification.d.ts +3 -1
- package/dist/operations/services/github-actions-verification.js +11 -3
- package/dist/operations/services/github-api.js +21 -5
- package/dist/operations/services/github-automation.d.ts +3 -3
- package/dist/operations/services/github-automation.js +15 -18
- package/dist/operations/services/github-credentials.js +2 -1
- package/dist/operations/services/hosted-service-checks.d.ts +19 -1
- package/dist/operations/services/hosted-service-checks.js +162 -11
- 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 +19 -14
- package/dist/operations/services/live-hosted-service-checks.d.ts +2 -1
- package/dist/operations/services/live-hosted-service-checks.js +193 -80
- 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-repositories.d.ts +37 -0
- package/dist/operations/services/managed-repositories.js +177 -0
- package/dist/operations/services/operations-runner-smoke.d.ts +3 -2
- package/dist/operations/services/operations-runner-smoke.js +22 -4
- package/dist/operations/services/package-adapters.d.ts +77 -2
- package/dist/operations/services/package-adapters.js +629 -56
- package/dist/operations/services/package-reference-policy.d.ts +12 -88
- package/dist/operations/services/package-reference-policy.js +81 -213
- package/dist/operations/services/project-host-operations.d.ts +4 -4
- package/dist/operations/services/project-platform.d.ts +19 -193
- package/dist/operations/services/project-platform.js +31 -103
- package/dist/operations/services/project-web-monitor.d.ts +2 -2
- package/dist/operations/services/project-web-monitor.js +97 -2
- package/dist/operations/services/railway-api.d.ts +36 -7
- package/dist/operations/services/railway-api.js +331 -100
- package/dist/operations/services/railway-deploy.d.ts +7 -95
- package/dist/operations/services/railway-deploy.js +334 -742
- 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 +6 -7
- package/dist/operations/services/repository-save-orchestrator.js +353 -174
- 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 -192
- package/dist/operations/services/runtime-tools.js +4 -444
- 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/workflow-timing.d.ts +25 -0
- package/dist/operations/services/workflow-timing.js +79 -0
- package/dist/operations/services/workspace-dependency-mode.js +41 -4
- 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 +41 -18
- package/dist/operations/services/workspace-tools.js +40 -6
- 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 +6 -1
- package/dist/platform/deploy-config.d.ts +1 -1
- package/dist/platform/deploy-config.js +7 -1
- package/dist/platform/deploy-runtime.d.ts +1 -1
- package/dist/platform/desired-state.d.ts +66 -0
- package/dist/platform/desired-state.js +1052 -0
- package/dist/platform/env.yaml +414 -19
- package/dist/platform/environment.d.ts +2 -2
- package/dist/platform/environment.js +54 -22
- 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 +5 -5
- 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/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 +3 -3
- package/dist/reconcile/builtin-adapters.d.ts +7 -1
- package/dist/reconcile/builtin-adapters.js +2924 -346
- package/dist/reconcile/contracts.d.ts +15 -2
- package/dist/reconcile/desired-state.d.ts +2 -193
- package/dist/reconcile/desired-state.js +24 -33
- package/dist/reconcile/engine.d.ts +60 -397
- package/dist/reconcile/engine.js +182 -18
- package/dist/reconcile/index.d.ts +10 -10
- package/dist/reconcile/live-acceptance.d.ts +22 -1
- package/dist/reconcile/live-acceptance.js +456 -44
- 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 +3 -40
- package/dist/reconcile/units.d.ts +1 -1
- package/dist/reconcile/units.js +7 -1
- 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/migrate-journey-csv-to-guarantees.js +140 -0
- package/dist/scripts/package-tools.js +12 -1
- package/dist/scripts/prepare.js +3 -7
- package/dist/scripts/release-verify.js +2 -5
- 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/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 +1184 -113
- package/dist/sdk-types.js +393 -0
- package/dist/sdk.d.ts +74 -79
- package/dist/sdk.js +27 -36
- 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-ui.d.ts +2 -2
- package/dist/treedx/adapters.d.ts +6 -6
- package/dist/treedx/client.d.ts +1 -1
- package/dist/treedx/errors.d.ts +1 -1
- package/dist/treedx/federated-client.d.ts +3 -3
- package/dist/treedx/graph-adapter.d.ts +2 -2
- package/dist/treedx/index.d.ts +14 -14
- package/dist/treedx/market-integration.d.ts +1 -1
- package/dist/treedx/ports.d.ts +30 -30
- package/dist/treedx/query-adapter.d.ts +3 -3
- package/dist/treedx/registry-client.d.ts +2 -2
- package/dist/treedx/repository-adapter.d.ts +4 -4
- package/dist/treedx/repository-adapter.js +14 -3
- package/dist/treedx/sdk-integration.d.ts +3 -3
- package/dist/treedx/types.d.ts +2 -2
- package/dist/treedx/workspace-adapter.d.ts +2 -2
- package/dist/treedx-backends.d.ts +13 -11
- package/dist/treedx-backends.js +52 -50
- package/dist/treedx-client.d.ts +3 -0
- package/dist/treedx-client.js +10 -1
- package/dist/treeseed/services/compose.yml +2 -3
- package/dist/treeseed/template-catalog/catalog.fixture.json +4 -4
- 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 +510 -559
- package/dist/workflow/operations.js +1891 -1342
- 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 +5 -5
- package/dist/workflow-state.js +27 -17
- package/dist/workflow-support.d.ts +26 -25
- package/dist/workflow-support.js +27 -31
- package/dist/workflow.d.ts +46 -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/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 +75 -18
- package/scripts/{verify-driver.mjs → verify-driver.ts} +19 -4
- package/templates/github/deploy-web.workflow.yml +12 -8
- 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 -60
- package/dist/operations/services/release-candidate.js +0 -953
- 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,
|
|
@@ -24,23 +34,18 @@ import {
|
|
|
24
34
|
setTreeseedRemoteSession,
|
|
25
35
|
writeTreeseedMachineConfig
|
|
26
36
|
} from "../operations/services/config-runtime.js";
|
|
27
|
-
import {
|
|
37
|
+
import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
28
38
|
import { ControlPlaneClient } from "../control-plane-client.js";
|
|
29
39
|
import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
|
|
30
40
|
import {
|
|
31
|
-
assertDeploymentInitialized,
|
|
32
41
|
buildProvisioningSummary,
|
|
33
42
|
cleanupDestroyedState,
|
|
34
43
|
createBranchPreviewDeployTarget,
|
|
35
44
|
createPersistentDeployTarget,
|
|
36
|
-
destroyCloudflareResources,
|
|
37
45
|
destroyTreeseedEnvironmentResources,
|
|
38
|
-
ensureGeneratedWranglerConfig,
|
|
39
|
-
finalizeDeploymentState,
|
|
40
46
|
loadDeployState,
|
|
41
47
|
recordHostedDeploymentState,
|
|
42
|
-
|
|
43
|
-
validateDeployPrerequisites,
|
|
48
|
+
resolveConfiguredSurfaceDomain,
|
|
44
49
|
validateDestroyPrerequisites
|
|
45
50
|
} from "../operations/services/deploy.js";
|
|
46
51
|
import {
|
|
@@ -49,7 +54,6 @@ import {
|
|
|
49
54
|
assertFeatureBranch,
|
|
50
55
|
branchExists,
|
|
51
56
|
checkoutBranch,
|
|
52
|
-
checkoutDetachedOriginBranch,
|
|
53
57
|
checkoutTaskBranchFromStaging,
|
|
54
58
|
createDeprecatedTaskTag,
|
|
55
59
|
deleteLocalBranch,
|
|
@@ -58,36 +62,36 @@ import {
|
|
|
58
62
|
gitWorkflowRoot,
|
|
59
63
|
headCommit,
|
|
60
64
|
listTaskBranches,
|
|
61
|
-
|
|
62
|
-
prepareReleaseBranches,
|
|
65
|
+
mergeBranchDownIntoFeature,
|
|
63
66
|
PRODUCTION_BRANCH,
|
|
67
|
+
promoteCommitToBranchWithExpectedHead,
|
|
64
68
|
pushBranch,
|
|
65
|
-
pushHeadToBranch,
|
|
66
69
|
reattachDetachedHeadIfSafe,
|
|
70
|
+
remoteHeadCommit,
|
|
67
71
|
remoteBranchExists,
|
|
68
72
|
STAGING_BRANCH,
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
syncBranchWithOrigin,
|
|
74
|
+
waitForStagingAutomation
|
|
71
75
|
} from "../operations/services/git-workflow.js";
|
|
72
76
|
import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
|
|
73
77
|
import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
|
|
74
|
-
import { dispatchGitHubWorkflowRun } from "../operations/services/github-api.js";
|
|
75
78
|
import {
|
|
76
79
|
formatGitHubActionsGateFailure,
|
|
77
80
|
inspectGitHubActionsVerification,
|
|
78
81
|
skippedGitHubActionsGate,
|
|
79
82
|
waitForGitHubActionsGate
|
|
80
83
|
} from "../operations/services/github-actions-verification.js";
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
} 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";
|
|
84
88
|
import {
|
|
85
89
|
collectReleaseHistoryCommits,
|
|
86
90
|
renderAdministrativeCommitMessage,
|
|
87
91
|
upsertReleaseChangelog
|
|
88
92
|
} from "../operations/services/release-history.js";
|
|
89
|
-
import {
|
|
90
|
-
import {
|
|
93
|
+
import { packageScriptPath } from "../operations/services/runtime-tools.js";
|
|
94
|
+
import { loadTreeseedPlatformConfig } from "../platform/config.js";
|
|
91
95
|
import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
|
|
92
96
|
import {
|
|
93
97
|
collectMergeConflictReport,
|
|
@@ -104,20 +108,14 @@ import {
|
|
|
104
108
|
} from "../operations/services/workspace-save.js";
|
|
105
109
|
import {
|
|
106
110
|
planRepositorySave,
|
|
107
|
-
refreshAndValidateRootWorkspaceLockfileForSave,
|
|
108
111
|
repositorySaveErrorDetails,
|
|
109
112
|
runRepositorySaveOrchestrator
|
|
110
113
|
} from "../operations/services/repository-save-orchestrator.js";
|
|
111
114
|
import { discoverTreeseedPackageAdapters } from "../operations/services/package-adapters.js";
|
|
112
115
|
import {
|
|
113
|
-
assertNoInternalDevReferences,
|
|
114
|
-
cleanupStaleTreeseedDevTags,
|
|
115
|
-
collectTreeseedDevTagCleanupPlan,
|
|
116
116
|
collectInternalDevReferenceIssues,
|
|
117
|
-
devTagFromDependencySpec,
|
|
118
117
|
installableInternalDependencyVersions,
|
|
119
|
-
normalizeGitRemoteForManifest
|
|
120
|
-
rewriteProjectInternalDependenciesToStableVersions
|
|
118
|
+
normalizeGitRemoteForManifest
|
|
121
119
|
} from "../operations/services/package-reference-policy.js";
|
|
122
120
|
import {
|
|
123
121
|
ensureLocalWorkspaceLinks,
|
|
@@ -125,7 +123,6 @@ import {
|
|
|
125
123
|
unlinkLocalWorkspaceLinks
|
|
126
124
|
} from "../operations/services/workspace-dependency-mode.js";
|
|
127
125
|
import {
|
|
128
|
-
hasCompleteTreeseedPackageCheckout,
|
|
129
126
|
changedWorkspacePackages,
|
|
130
127
|
publishableWorkspacePackages,
|
|
131
128
|
run,
|
|
@@ -133,13 +130,13 @@ import {
|
|
|
133
130
|
workspacePackages,
|
|
134
131
|
workspaceRoot
|
|
135
132
|
} from "../operations/services/workspace-tools.js";
|
|
136
|
-
import {
|
|
137
|
-
import { collectTreeseedHostedServiceChecks } from "../operations/services/hosted-service-checks.js";
|
|
133
|
+
import { classifyTreeseedGitMode, runTreeseedGit, runTreeseedGitOk, runTreeseedGitText } from "../operations/services/git-runner.js";
|
|
138
134
|
import { collectTreeseedDeploymentReadiness } from "../operations/services/deployment-readiness.js";
|
|
139
135
|
import { collectTreeseedLiveHostedServiceChecks } from "../operations/services/live-hosted-service-checks.js";
|
|
140
136
|
import { discoverTreeseedApplications } from "../hosting/apps.js";
|
|
137
|
+
import { compileTreeseedHostingGraph } from "../hosting/graph.js";
|
|
141
138
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
142
|
-
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits,
|
|
139
|
+
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, destroyTreeseedTargetUnits, filterTreeseedDesiredUnitsByBootstrapSystems, resolveTreeseedBootstrapSelection } from "../reconcile/index.js";
|
|
143
140
|
import {
|
|
144
141
|
acquireWorkflowLock,
|
|
145
142
|
archiveWorkflowRun,
|
|
@@ -161,6 +158,7 @@ import {
|
|
|
161
158
|
checkedOutWorkspacePackageRepos,
|
|
162
159
|
resolveTreeseedWorkflowSession
|
|
163
160
|
} from "./session.js";
|
|
161
|
+
import { checkedOutManagedWorkflowRepos } from "../operations/services/managed-repositories.js";
|
|
164
162
|
import {
|
|
165
163
|
classifyTreeseedBranchRole,
|
|
166
164
|
resolveTreeseedWorkflowPaths
|
|
@@ -192,6 +190,14 @@ function defaultWrite(output, stream = "stdout") {
|
|
|
192
190
|
(stream === "stderr" ? process.stderr : process.stdout).write(`${output}
|
|
193
191
|
`);
|
|
194
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
|
+
}
|
|
195
201
|
function shouldManageWorkspaceLinks(mode, env = process.env) {
|
|
196
202
|
if (mode === "off") return false;
|
|
197
203
|
const envMode = String(env?.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
|
|
@@ -223,7 +229,7 @@ function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
|
|
|
223
229
|
const packages = [
|
|
224
230
|
{ name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
|
|
225
231
|
{ name: "@treeseed/ui", dir: "packages/ui", artifacts: ["dist/index.js"] },
|
|
226
|
-
{ name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/
|
|
232
|
+
{ name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/manager.js", "dist/provider/runner.js"] },
|
|
227
233
|
{ name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
|
|
228
234
|
{ name: "@treeseed/admin", dir: "packages/admin", artifacts: ["dist/plugin.js"] },
|
|
229
235
|
{ name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
|
|
@@ -267,7 +273,7 @@ function ensureWorkflowCommandBins(root, helpers) {
|
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
function unresolvedMergePaths(repoDir) {
|
|
270
|
-
return
|
|
276
|
+
return runGit(["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
271
277
|
}
|
|
272
278
|
function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
273
279
|
const gitRoot = repoRoot(root);
|
|
@@ -288,7 +294,7 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
|
288
294
|
const entries = [];
|
|
289
295
|
for (const pkg of packages) {
|
|
290
296
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
291
|
-
|
|
297
|
+
runGit(["add", pkg.repoPath], { cwd: gitRoot });
|
|
292
298
|
entries.push({
|
|
293
299
|
packageName: pkg.name,
|
|
294
300
|
path: pkg.repoPath,
|
|
@@ -323,7 +329,7 @@ function normalizeSaveLane(lane) {
|
|
|
323
329
|
}
|
|
324
330
|
function normalizeSaveCiMode(mode, branch, lane = "fast") {
|
|
325
331
|
if (mode === "hosted" || mode === "off") return mode;
|
|
326
|
-
if (lane === "promotion") return branch === STAGING_BRANCH ? "hosted" : "off";
|
|
332
|
+
if (lane === "promotion") return branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH ? "hosted" : "off";
|
|
327
333
|
return "off";
|
|
328
334
|
}
|
|
329
335
|
function normalizeSaveVerifyMode(mode) {
|
|
@@ -377,35 +383,27 @@ function helpersForCwd(helpers, cwd) {
|
|
|
377
383
|
function shouldDispatchSwitchToManagedWorktree(root, input, env) {
|
|
378
384
|
return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
|
|
379
385
|
}
|
|
380
|
-
function
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"github_auth_unavailable",
|
|
391
|
-
[
|
|
392
|
-
"Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
|
|
393
|
-
github.detail,
|
|
394
|
-
`Managed gh check: ${command}`,
|
|
395
|
-
"Remediation:",
|
|
396
|
-
...github.remediation.map((item) => `- ${item}`)
|
|
397
|
-
].join("\n"),
|
|
398
|
-
{
|
|
399
|
-
details: {
|
|
400
|
-
toolsHome: tools.toolsHome,
|
|
401
|
-
ghConfigDir: tools.ghConfigDir,
|
|
402
|
-
github,
|
|
403
|
-
tools: tools.tools
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
);
|
|
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
|
+
}
|
|
407
396
|
}
|
|
408
|
-
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
|
+
);
|
|
409
407
|
}
|
|
410
408
|
async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
411
409
|
if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
@@ -414,42 +412,49 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
414
412
|
if (gates.length === 0) {
|
|
415
413
|
return [];
|
|
416
414
|
}
|
|
417
|
-
|
|
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);
|
|
418
419
|
const results = [];
|
|
419
420
|
for (const gate of gates) {
|
|
421
|
+
const gateWithTimeout = {
|
|
422
|
+
...gate,
|
|
423
|
+
timeoutSeconds: gate.timeoutSeconds ?? HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS
|
|
424
|
+
};
|
|
420
425
|
if (options.root && options.runId) {
|
|
421
426
|
const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
|
|
422
|
-
repository:
|
|
423
|
-
workflow:
|
|
424
|
-
headSha:
|
|
425
|
-
branch:
|
|
427
|
+
repository: gateWithTimeout.repository ?? null,
|
|
428
|
+
workflow: gateWithTimeout.workflow,
|
|
429
|
+
headSha: gateWithTimeout.headSha,
|
|
430
|
+
branch: gateWithTimeout.branch
|
|
426
431
|
});
|
|
427
432
|
if (cached) {
|
|
428
433
|
results.push({
|
|
429
434
|
...cached.result,
|
|
430
|
-
name:
|
|
435
|
+
name: gateWithTimeout.name,
|
|
431
436
|
cached: true
|
|
432
437
|
});
|
|
433
438
|
continue;
|
|
434
439
|
}
|
|
435
440
|
}
|
|
436
|
-
const result = await waitForGitHubActionsGate(
|
|
441
|
+
const result = await waitForGitHubActionsGate(gateWithTimeout, {
|
|
437
442
|
operation,
|
|
438
|
-
env: githubWorkflowGateEnv(options.root,
|
|
443
|
+
env: githubWorkflowGateEnv(options.root, gateWithTimeout),
|
|
439
444
|
onProgress: options.onProgress
|
|
440
445
|
});
|
|
441
446
|
const normalized = {
|
|
442
|
-
name:
|
|
447
|
+
name: gateWithTimeout.name,
|
|
443
448
|
...result,
|
|
444
|
-
workflow: String(result.workflow ??
|
|
445
|
-
branch: String(result.branch ??
|
|
446
|
-
headSha: String(result.headSha ??
|
|
447
|
-
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,
|
|
448
453
|
cached: false
|
|
449
454
|
};
|
|
450
455
|
if (normalized.status === "completed" && normalized.conclusion !== "success") {
|
|
451
|
-
workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(
|
|
452
|
-
details: { gate, workflow: normalized }
|
|
456
|
+
workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gateWithTimeout, normalized), {
|
|
457
|
+
details: { gate: gateWithTimeout, workflow: normalized }
|
|
453
458
|
});
|
|
454
459
|
}
|
|
455
460
|
if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
|
|
@@ -469,6 +474,7 @@ function githubWorkflowGateEnv(root, gate) {
|
|
|
469
474
|
if (!credential.token) return process.env;
|
|
470
475
|
return {
|
|
471
476
|
...process.env,
|
|
477
|
+
TREESEED_GITHUB_TOKEN: credential.token,
|
|
472
478
|
GH_TOKEN: credential.token,
|
|
473
479
|
GITHUB_TOKEN: credential.token
|
|
474
480
|
};
|
|
@@ -477,12 +483,127 @@ function githubWorkflowGateEnv(root, gate) {
|
|
|
477
483
|
}
|
|
478
484
|
}
|
|
479
485
|
const HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
|
|
486
|
+
const HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS = 45 * 60;
|
|
480
487
|
function hostedDeployGate(gate) {
|
|
481
488
|
return {
|
|
482
489
|
...gate,
|
|
483
490
|
timeoutSeconds: gate.timeoutSeconds ?? HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS
|
|
484
491
|
};
|
|
485
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
|
+
}
|
|
486
607
|
function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
|
|
487
608
|
const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
|
|
488
609
|
const releaseRecord = stringRecord(rootRelease) ?? {};
|
|
@@ -548,6 +669,22 @@ ${missing.map((check) => `${check.id}: ${check.path}`).join("\n")}`, {
|
|
|
548
669
|
}
|
|
549
670
|
return report;
|
|
550
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
|
+
}
|
|
551
688
|
function workflowError(operation, code, message, options = {}) {
|
|
552
689
|
throw new TreeseedWorkflowError(operation, code, message, options);
|
|
553
690
|
}
|
|
@@ -650,6 +787,9 @@ function createWorkspaceRootRepoReport(root) {
|
|
|
650
787
|
function createWorkspacePackageReports(root) {
|
|
651
788
|
return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
|
|
652
789
|
}
|
|
790
|
+
function createManagedWorkflowRepoReports(root) {
|
|
791
|
+
return checkedOutManagedWorkflowRepos(root).map((repo) => createRepoReport(repo.name, repo.dir, currentBranch(repo.dir) || null, hasMeaningfulChanges(repo.dir)));
|
|
792
|
+
}
|
|
653
793
|
function findReportByName(reports, name) {
|
|
654
794
|
return reports.find((report) => report.name === name) ?? null;
|
|
655
795
|
}
|
|
@@ -657,15 +797,20 @@ function findReportByPath(reports, path) {
|
|
|
657
797
|
return reports.find((report) => report.path === path) ?? null;
|
|
658
798
|
}
|
|
659
799
|
function assertWorkspaceClean(root) {
|
|
660
|
-
const repoDirs = [repoRoot(root), ...
|
|
800
|
+
const repoDirs = [repoRoot(root), ...checkedOutManagedWorkflowRepos(root).map((repo) => repo.dir)];
|
|
661
801
|
assertCleanWorktrees(repoDirs);
|
|
662
802
|
return repoDirs;
|
|
663
803
|
}
|
|
664
804
|
function buildWorkflowResult(operation, cwd, payload, options = {}) {
|
|
805
|
+
const timing = options.timing ?? createTreeseedWorkflowTimer().finish();
|
|
665
806
|
const resolvedPayload = options.includeFinalState ?? true ? {
|
|
666
807
|
...payload,
|
|
808
|
+
timing,
|
|
667
809
|
finalState: resolveWorkflowStateSnapshot(cwd)
|
|
668
|
-
} :
|
|
810
|
+
} : {
|
|
811
|
+
...payload,
|
|
812
|
+
timing
|
|
813
|
+
};
|
|
669
814
|
return {
|
|
670
815
|
schemaVersion: 1,
|
|
671
816
|
kind: "treeseed.workflow.result",
|
|
@@ -756,7 +901,7 @@ function selectWorkflowApplications(root, input = {}) {
|
|
|
756
901
|
function singleSelectedWorkflowAppId(selection) {
|
|
757
902
|
return selection.selected.length === 1 ? selection.selected[0] : void 0;
|
|
758
903
|
}
|
|
759
|
-
async function
|
|
904
|
+
async function workflowHostedVerificationGateRequired(operation, root, helpers, environment, options = { enabled: true }) {
|
|
760
905
|
if (!options.enabled) return null;
|
|
761
906
|
const target = environment === "prod" ? "prod" : environment === "local" ? "local" : "staging";
|
|
762
907
|
const readiness = collectTreeseedDeploymentReadiness({
|
|
@@ -771,53 +916,20 @@ ${failures.join("\n")}`, {
|
|
|
771
916
|
details: { readiness }
|
|
772
917
|
});
|
|
773
918
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
write: (line) => helpers.write(line)
|
|
781
|
-
});
|
|
782
|
-
const hostedServices = options.live ? await collectTreeseedLiveHostedServiceChecks({
|
|
783
|
-
tenantRoot: root,
|
|
784
|
-
target,
|
|
785
|
-
strict: options.strict === true,
|
|
786
|
-
requireLiveRailway: options.strict === true,
|
|
787
|
-
requireLiveHttp: options.strict === true,
|
|
788
|
-
appId: options.appId,
|
|
789
|
-
retry: { attempts: 12, intervalMs: 1e4 },
|
|
790
|
-
env: helpers.context.env
|
|
791
|
-
}) : collectTreeseedHostedServiceChecks({
|
|
792
|
-
tenantRoot: root,
|
|
793
|
-
target,
|
|
794
|
-
appId: options.appId
|
|
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
|
+
}
|
|
795
925
|
});
|
|
796
|
-
if (options.strict && !hostingAudit.ok) {
|
|
797
|
-
workflowError(operation, "validation_failed", `Hosting audit failed for ${hostingAudit.environment}: ${hostingAudit.blockers.join("\n")}`, {
|
|
798
|
-
details: { hostingAudit, hostedServices, readiness }
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
if (options.strict && hostedServices.summary.failed > 0) {
|
|
802
|
-
const failures = hostedServices.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || check.description}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
|
|
803
|
-
workflowError(operation, "validation_failed", `Hosted service checks failed for ${hostedServices.target}:
|
|
804
|
-
${failures.join("\n")}`, {
|
|
805
|
-
details: { hostingAudit, hostedServices, readiness }
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
hostingAudit,
|
|
810
|
-
hostedServices,
|
|
811
|
-
readiness,
|
|
812
|
-
live: options.live === true
|
|
813
|
-
};
|
|
814
926
|
}
|
|
815
927
|
function normalizeExecutionMode(input) {
|
|
816
928
|
return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
|
|
817
929
|
}
|
|
818
930
|
function submodulePointerForRef(repoDir, ref, relativeDir) {
|
|
819
931
|
try {
|
|
820
|
-
const output =
|
|
932
|
+
const output = runGit(["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
|
|
821
933
|
if (!output) {
|
|
822
934
|
return null;
|
|
823
935
|
}
|
|
@@ -905,13 +1017,13 @@ function stablePackageGitReferences(root, versions) {
|
|
|
905
1017
|
}
|
|
906
1018
|
function gitObjectCommit(repoDir, ref) {
|
|
907
1019
|
try {
|
|
908
|
-
return
|
|
1020
|
+
return runGit(["rev-list", "-n", "1", ref], { cwd: repoDir, capture: true }).trim() || null;
|
|
909
1021
|
} catch {
|
|
910
1022
|
return null;
|
|
911
1023
|
}
|
|
912
1024
|
}
|
|
913
1025
|
function remoteTagCommit(repoDir, tagName) {
|
|
914
|
-
const output =
|
|
1026
|
+
const output = runGit(["ls-remote", "origin", `refs/tags/${tagName}`, `refs/tags/${tagName}^{}`], { cwd: repoDir, capture: true }).trim();
|
|
915
1027
|
if (!output) return null;
|
|
916
1028
|
const peeled = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
|
|
917
1029
|
const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
|
|
@@ -923,14 +1035,14 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
|
923
1035
|
throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
|
|
924
1036
|
}
|
|
925
1037
|
if (!localCommit) {
|
|
926
|
-
|
|
1038
|
+
runGit(["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
|
|
927
1039
|
}
|
|
928
1040
|
const remoteCommit = remoteTagCommit(repoDir, tagName);
|
|
929
1041
|
if (remoteCommit && remoteCommit !== commitSha) {
|
|
930
1042
|
throw new Error(`Release tag ${tagName} already exists on origin at ${remoteCommit}, expected ${commitSha}.`);
|
|
931
1043
|
}
|
|
932
1044
|
if (!remoteCommit) {
|
|
933
|
-
|
|
1045
|
+
runGit(["push", "origin", tagName], { cwd: repoDir });
|
|
934
1046
|
}
|
|
935
1047
|
return {
|
|
936
1048
|
tagName,
|
|
@@ -938,12 +1050,28 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
|
938
1050
|
remote: remoteCommit ? "existing" : "pushed"
|
|
939
1051
|
};
|
|
940
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
|
+
}
|
|
941
1069
|
function commitAllIfChanged(repoDir, message) {
|
|
942
|
-
|
|
1070
|
+
runGit(["add", "-A"], { cwd: repoDir });
|
|
943
1071
|
if (!hasMeaningfulChanges(repoDir)) {
|
|
944
1072
|
return { committed: false, commitSha: headCommit(repoDir) };
|
|
945
1073
|
}
|
|
946
|
-
|
|
1074
|
+
runGit(["commit", "-m", message], { cwd: repoDir });
|
|
947
1075
|
return { committed: true, commitSha: headCommit(repoDir) };
|
|
948
1076
|
}
|
|
949
1077
|
function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
|
|
@@ -1021,6 +1149,11 @@ function defaultCiWorkflows(kind, branch) {
|
|
|
1021
1149
|
}
|
|
1022
1150
|
return ["verify.yml"];
|
|
1023
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
|
+
}
|
|
1024
1157
|
function githubRepositoryForRepo(repoDir) {
|
|
1025
1158
|
try {
|
|
1026
1159
|
return resolveGitHubRepositorySlug(repoDir);
|
|
@@ -1030,7 +1163,7 @@ function githubRepositoryForRepo(repoDir) {
|
|
|
1030
1163
|
}
|
|
1031
1164
|
function ciTargetForRepo(repo, kind, input, workflowOverrides) {
|
|
1032
1165
|
const branch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : repo.branchName;
|
|
1033
|
-
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);
|
|
1034
1167
|
return {
|
|
1035
1168
|
name: repo.name,
|
|
1036
1169
|
repoPath: repo.path,
|
|
@@ -1083,7 +1216,7 @@ async function createCiResult(cwd, input) {
|
|
|
1083
1216
|
function createTasksResult(cwd) {
|
|
1084
1217
|
const tenantRoot = cwd;
|
|
1085
1218
|
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
1086
|
-
const deployConfig =
|
|
1219
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
|
|
1087
1220
|
const dirty = gitStatusPorcelain(repoDir).length > 0;
|
|
1088
1221
|
const tasks = listTaskBranches(repoDir).map((branch) => {
|
|
1089
1222
|
const previewState = loadDeployState(tenantRoot, deployConfig, {
|
|
@@ -1332,7 +1465,32 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
1332
1465
|
if (branch === STAGING_BRANCH && (hasMeaningfulChanges(repoRoot(root)) || checkedOutWorkspacePackageRepos(root).some((repo) => hasMeaningfulChanges(repo.dir)))) {
|
|
1333
1466
|
return null;
|
|
1334
1467
|
}
|
|
1335
|
-
|
|
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;
|
|
1336
1494
|
}
|
|
1337
1495
|
function workflowFileExists(repoPath, workflow) {
|
|
1338
1496
|
return existsSync(resolve(repoPath, ".github", "workflows", workflow));
|
|
@@ -1426,7 +1584,7 @@ function generatedReleaseMetadataFiles(repoDir) {
|
|
|
1426
1584
|
return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
|
|
1427
1585
|
if (existsSync(resolve(repoDir, filePath))) return true;
|
|
1428
1586
|
try {
|
|
1429
|
-
|
|
1587
|
+
runGit(["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
|
|
1430
1588
|
return true;
|
|
1431
1589
|
} catch {
|
|
1432
1590
|
return false;
|
|
@@ -1455,12 +1613,12 @@ function restoreReleaseGeneratedMetadata(repo) {
|
|
|
1455
1613
|
const restored = [];
|
|
1456
1614
|
const skipped = [];
|
|
1457
1615
|
for (const filePath of repo.files) {
|
|
1458
|
-
const status =
|
|
1616
|
+
const status = runGit(["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
|
|
1459
1617
|
if (!status.trim()) {
|
|
1460
1618
|
skipped.push(filePath);
|
|
1461
1619
|
continue;
|
|
1462
1620
|
}
|
|
1463
|
-
|
|
1621
|
+
runGit(["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
|
|
1464
1622
|
restored.push(filePath);
|
|
1465
1623
|
}
|
|
1466
1624
|
return { restored, skipped };
|
|
@@ -1697,7 +1855,12 @@ function failWorkflowRun(root, runId, error, recovery) {
|
|
|
1697
1855
|
releaseWorkflowLock(root, runId);
|
|
1698
1856
|
}
|
|
1699
1857
|
function validatePackageReleaseWorkflows(root, packageNames) {
|
|
1700
|
-
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);
|
|
1701
1864
|
if (missing.length > 0) {
|
|
1702
1865
|
workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
|
|
1703
1866
|
details: {
|
|
@@ -1706,6 +1869,26 @@ function validatePackageReleaseWorkflows(root, packageNames) {
|
|
|
1706
1869
|
});
|
|
1707
1870
|
}
|
|
1708
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
|
+
}
|
|
1709
1892
|
function validateStagingWorkflowContracts(root) {
|
|
1710
1893
|
if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
1711
1894
|
return;
|
|
@@ -1725,17 +1908,38 @@ function validateStagingWorkflowContracts(root) {
|
|
|
1725
1908
|
function shouldSkipReleaseInstall() {
|
|
1726
1909
|
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
|
|
1727
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
|
+
}
|
|
1728
1925
|
function runReleaseNpmInstall(repoDir, options = {}) {
|
|
1729
1926
|
if (shouldSkipReleaseInstall()) {
|
|
1730
1927
|
return { status: "skipped", reason: "disabled" };
|
|
1731
1928
|
}
|
|
1732
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"];
|
|
1733
|
-
const
|
|
1930
|
+
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
1931
|
+
const result = spawnSync(spawnCommand.command, spawnCommand.args, {
|
|
1734
1932
|
cwd: repoDir,
|
|
1735
1933
|
env: {
|
|
1736
1934
|
...process.env,
|
|
1737
1935
|
npm_config_audit: "false",
|
|
1738
|
-
|
|
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"
|
|
1739
1943
|
},
|
|
1740
1944
|
stdio: "pipe",
|
|
1741
1945
|
encoding: "utf8"
|
|
@@ -1770,7 +1974,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
|
|
|
1770
1974
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
1771
1975
|
checkoutBranch(repoDir, STAGING_BRANCH);
|
|
1772
1976
|
try {
|
|
1773
|
-
|
|
1977
|
+
runGit(["merge-base", "--is-ancestor", `origin/${PRODUCTION_BRANCH}`, "HEAD"], { cwd: repoDir, capture: true });
|
|
1774
1978
|
return {
|
|
1775
1979
|
status: "up-to-date",
|
|
1776
1980
|
merged: false,
|
|
@@ -1782,7 +1986,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
|
|
|
1782
1986
|
} catch {
|
|
1783
1987
|
}
|
|
1784
1988
|
try {
|
|
1785
|
-
|
|
1989
|
+
runGit(["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
|
|
1786
1990
|
} catch (error) {
|
|
1787
1991
|
const report = collectMergeConflictReport(repoDir);
|
|
1788
1992
|
throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
@@ -1835,69 +2039,6 @@ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, optio
|
|
|
1835
2039
|
packageStagingPointerCommit: pointerSync.commitSha
|
|
1836
2040
|
};
|
|
1837
2041
|
}
|
|
1838
|
-
function collectActiveDevTagReferences(root) {
|
|
1839
|
-
return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
|
|
1840
|
-
}
|
|
1841
|
-
function normalizeIncludePackages(value) {
|
|
1842
|
-
if (!value) return null;
|
|
1843
|
-
const values = Array.isArray(value) ? value : String(value).split(",");
|
|
1844
|
-
const normalized = values.map((entry) => String(entry).trim()).filter(Boolean);
|
|
1845
|
-
return normalized.length > 0 ? new Set(normalized) : null;
|
|
1846
|
-
}
|
|
1847
|
-
function normalizeDevTagBranchScope(value) {
|
|
1848
|
-
return value === "staging" || value === "preview" || value === "all" ? value : "all";
|
|
1849
|
-
}
|
|
1850
|
-
function packageJsonVersion(repoDir) {
|
|
1851
|
-
const packageJson = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
|
|
1852
|
-
return String(packageJson.version ?? "");
|
|
1853
|
-
}
|
|
1854
|
-
function collectStaleDevTagCleanupReports(root, input, options) {
|
|
1855
|
-
const includePackages = normalizeIncludePackages(input.includePackages);
|
|
1856
|
-
const branchScope = normalizeDevTagBranchScope(input.branchScope);
|
|
1857
|
-
const activeDevTags = collectActiveDevTagReferences(root);
|
|
1858
|
-
const repos = checkedOutWorkspacePackageRepos(root).filter((pkg) => !includePackages || includePackages.has(pkg.name)).map((pkg) => {
|
|
1859
|
-
const currentVersion = packageJsonVersion(pkg.dir);
|
|
1860
|
-
const report = options.execute ? cleanupStaleTreeseedDevTags({
|
|
1861
|
-
repoDir: pkg.dir,
|
|
1862
|
-
packageName: pkg.name,
|
|
1863
|
-
currentVersion,
|
|
1864
|
-
activeReferences: activeDevTags,
|
|
1865
|
-
branchScope
|
|
1866
|
-
}) : {
|
|
1867
|
-
...collectTreeseedDevTagCleanupPlan({
|
|
1868
|
-
repoDir: pkg.dir,
|
|
1869
|
-
packageName: pkg.name,
|
|
1870
|
-
currentVersion,
|
|
1871
|
-
activeReferences: activeDevTags,
|
|
1872
|
-
branchScope
|
|
1873
|
-
}),
|
|
1874
|
-
status: "planned",
|
|
1875
|
-
candidateCount: 0,
|
|
1876
|
-
cleaned: [],
|
|
1877
|
-
cleanedCount: 0,
|
|
1878
|
-
skippedCount: 0
|
|
1879
|
-
};
|
|
1880
|
-
if (!options.execute) {
|
|
1881
|
-
const planned = report;
|
|
1882
|
-
planned.candidateCount = planned.candidates.length;
|
|
1883
|
-
planned.skippedCount = planned.skipped.length;
|
|
1884
|
-
}
|
|
1885
|
-
return {
|
|
1886
|
-
name: pkg.name,
|
|
1887
|
-
path: relative(root, pkg.dir),
|
|
1888
|
-
...report
|
|
1889
|
-
};
|
|
1890
|
-
});
|
|
1891
|
-
return {
|
|
1892
|
-
status: options.execute ? "completed" : "planned",
|
|
1893
|
-
branchScope,
|
|
1894
|
-
includePackages: includePackages ? [...includePackages].sort() : [],
|
|
1895
|
-
repos,
|
|
1896
|
-
candidateCount: repos.reduce((total, repo) => total + Number(repo.candidateCount ?? 0), 0),
|
|
1897
|
-
cleanedCount: repos.reduce((total, repo) => total + Number(repo.cleanedCount ?? 0), 0),
|
|
1898
|
-
skippedCount: repos.reduce((total, repo) => total + Number(repo.skippedCount ?? 0), 0)
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
2042
|
function releasePlanVersionMap(plannedVersions) {
|
|
1902
2043
|
return new Map(
|
|
1903
2044
|
Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
|
|
@@ -1929,38 +2070,233 @@ function stableDependencyVersionsForReleaseLine(root, options) {
|
|
|
1929
2070
|
}
|
|
1930
2071
|
return versions;
|
|
1931
2072
|
}
|
|
1932
|
-
function
|
|
1933
|
-
if (
|
|
1934
|
-
|
|
1935
|
-
}
|
|
1936
|
-
const rendered = report.failures.map((failure) => `- ${failure.scope}${failure.provider ? ` ${failure.provider}` : ""}: ${failure.message}`);
|
|
1937
|
-
workflowError(operation, "validation_failed", [
|
|
1938
|
-
"Treeseed release-candidate readiness failed.",
|
|
1939
|
-
...rendered
|
|
1940
|
-
].join("\n"), {
|
|
1941
|
-
details: {
|
|
1942
|
-
releaseCandidate: report
|
|
1943
|
-
}
|
|
1944
|
-
});
|
|
2073
|
+
function releaseCandidateProofDriver(mode, lane = "fast") {
|
|
2074
|
+
if (lane === "promotion" || mode === "strict") return "github-hosted";
|
|
2075
|
+
return "local";
|
|
1945
2076
|
}
|
|
1946
|
-
async function
|
|
1947
|
-
const plannedVersions = plannedRelease.plannedVersions && typeof plannedRelease.plannedVersions === "object" && !Array.isArray(plannedRelease.plannedVersions) ? plannedRelease.plannedVersions : {};
|
|
1948
|
-
const stableDependencyVersions = plannedRelease.stableDependencyVersions && typeof plannedRelease.stableDependencyVersions === "object" && !Array.isArray(plannedRelease.stableDependencyVersions) ? plannedRelease.stableDependencyVersions : {};
|
|
2077
|
+
async function runReleaseCandidateProofForPlan(operation, root, plannedRelease, options = {}) {
|
|
1949
2078
|
const packageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
|
|
1950
|
-
const
|
|
2079
|
+
const mode = options.mode ?? normalizeReleaseCandidateMode(void 0, operation);
|
|
2080
|
+
const driver = releaseCandidateProofDriver(mode, options.lane ?? "fast");
|
|
2081
|
+
const proof = await runTreeseedProof({
|
|
1951
2082
|
root,
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
mode: options.mode ?? normalizeReleaseCandidateMode(void 0, operation)
|
|
2083
|
+
target: operation === "release" ? "prod" : "staging",
|
|
2084
|
+
driver,
|
|
2085
|
+
write: options.write
|
|
1956
2086
|
});
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
+
}
|
|
1959
2288
|
}
|
|
1960
2289
|
function buildReleasePlanSnapshot(input) {
|
|
1961
2290
|
const selectedPackageNames = new Set(input.packageSelection.selected);
|
|
1962
2291
|
const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
|
|
1963
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
|
+
}
|
|
1964
2300
|
const plannedSelected = [...versionPlan.selected].filter((name) => versionPlan.versions.has(name));
|
|
1965
2301
|
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(input.packageSelection.changed.filter((name) => plannedSelected.includes(name))));
|
|
1966
2302
|
const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
|
|
@@ -1999,7 +2335,7 @@ function buildReleasePlanSnapshot(input) {
|
|
|
1999
2335
|
plannedDevReferenceRewrites,
|
|
2000
2336
|
plannedPublishWaits: plannedPackageSelection.selected.map((name) => ({
|
|
2001
2337
|
name,
|
|
2002
|
-
workflow:
|
|
2338
|
+
workflow: releaseWorkflowForPackage(input.root, name),
|
|
2003
2339
|
branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
|
|
2004
2340
|
status: "planned"
|
|
2005
2341
|
})),
|
|
@@ -2018,7 +2354,6 @@ function buildReleasePlanSnapshot(input) {
|
|
|
2018
2354
|
})),
|
|
2019
2355
|
{ id: "release-root", description: `Release market ${rootVersion}` },
|
|
2020
2356
|
{ id: "release-back-merge", description: "Back-merge production release history into staging" },
|
|
2021
|
-
{ id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
|
|
2022
2357
|
{ id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
|
|
2023
2358
|
],
|
|
2024
2359
|
blockers: input.blockers
|
|
@@ -2059,10 +2394,26 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames, ciMode)
|
|
|
2059
2394
|
if (ciMode === "off") {
|
|
2060
2395
|
return;
|
|
2061
2396
|
}
|
|
2062
|
-
|
|
2397
|
+
const values = resolveTreeseedMachineEnvironmentValues(root, "prod");
|
|
2398
|
+
const missing = [];
|
|
2063
2399
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2064
2400
|
if (!selectedPackageNames.has(pkg.name)) continue;
|
|
2065
|
-
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
|
+
);
|
|
2066
2417
|
}
|
|
2067
2418
|
}
|
|
2068
2419
|
function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
|
|
@@ -2089,31 +2440,31 @@ function assertSessionBranchSafety(operation, session, {
|
|
|
2089
2440
|
requireCurrentBranch = false,
|
|
2090
2441
|
allowPackageReposWithoutOrigin = false
|
|
2091
2442
|
} = {}) {
|
|
2092
|
-
const detached = session.
|
|
2443
|
+
const detached = session.managedRepos.filter((repo) => repo.kind !== "fixture" && repo.detached).map((repo) => repo.name);
|
|
2093
2444
|
if (detached.length > 0) {
|
|
2094
|
-
workflowError(operation, "validation_failed", `Detached
|
|
2445
|
+
workflowError(operation, "validation_failed", `Detached managed repository heads detected: ${detached.join(", ")}.`, {
|
|
2095
2446
|
details: { detached }
|
|
2096
2447
|
});
|
|
2097
2448
|
}
|
|
2098
2449
|
if (requireCleanPackages) {
|
|
2099
|
-
const dirty = session.
|
|
2450
|
+
const dirty = session.managedRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
2100
2451
|
if (dirty.length > 0) {
|
|
2101
|
-
workflowError(operation, "validation_failed", `Dirty
|
|
2452
|
+
workflowError(operation, "validation_failed", `Dirty managed repos block ${operation}: ${dirty.join(", ")}.`, {
|
|
2102
2453
|
details: { dirty }
|
|
2103
2454
|
});
|
|
2104
2455
|
}
|
|
2105
2456
|
}
|
|
2106
2457
|
if (requireCurrentBranch && session.branchName) {
|
|
2107
|
-
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 }));
|
|
2108
2459
|
if (missing.length > 0) {
|
|
2109
|
-
workflowError(operation, "validation_failed", `
|
|
2460
|
+
workflowError(operation, "validation_failed", `Managed repository branch alignment is required for ${operation}.`, {
|
|
2110
2461
|
details: { expectedBranch: session.branchName, repos: missing }
|
|
2111
2462
|
});
|
|
2112
2463
|
}
|
|
2113
2464
|
}
|
|
2114
2465
|
const missingOriginRepos = [
|
|
2115
2466
|
session.rootRepo,
|
|
2116
|
-
...allowPackageReposWithoutOrigin ? [] : session.
|
|
2467
|
+
...allowPackageReposWithoutOrigin ? [] : session.managedRepos
|
|
2117
2468
|
].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
|
|
2118
2469
|
if (missingOriginRepos.length > 0 && operation !== "destroy") {
|
|
2119
2470
|
workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
|
|
@@ -2122,7 +2473,7 @@ function assertSessionBranchSafety(operation, session, {
|
|
|
2122
2473
|
}
|
|
2123
2474
|
}
|
|
2124
2475
|
function previewStateFor(tenantRoot, branchName) {
|
|
2125
|
-
const deployConfig =
|
|
2476
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
|
|
2126
2477
|
return loadDeployState(tenantRoot, deployConfig, {
|
|
2127
2478
|
target: createBranchPreviewDeployTarget(branchName)
|
|
2128
2479
|
});
|
|
@@ -2135,59 +2486,71 @@ function branchPreviewInitialized(tenantRoot, branchName) {
|
|
|
2135
2486
|
return false;
|
|
2136
2487
|
}
|
|
2137
2488
|
}
|
|
2138
|
-
async function
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
env:
|
|
2162
|
-
|
|
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
|
|
2163
2516
|
});
|
|
2164
|
-
if ((deployResult.status ?? 1) !== 0) {
|
|
2165
|
-
workflowError("switch", "unsupported_state", "Preview deployment failed.", {
|
|
2166
|
-
exitCode: deployResult.status ?? 1,
|
|
2167
|
-
details: { branchName, wranglerPath }
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2170
|
-
const state = finalizeDeploymentState(tenantRoot, { target });
|
|
2171
2517
|
return {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2518
|
+
status: input.dryRun || !input.execute ? "planned" : "reconciled",
|
|
2519
|
+
branch: input.branch,
|
|
2520
|
+
initialize: input.initialize === true,
|
|
2521
|
+
reconcile
|
|
2176
2522
|
};
|
|
2177
2523
|
}
|
|
2178
|
-
function
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
|
|
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
|
+
});
|
|
2188
2549
|
return {
|
|
2189
|
-
|
|
2190
|
-
|
|
2550
|
+
status: input.dryRun || !input.execute ? "planned" : "destroyed",
|
|
2551
|
+
branch: input.branch,
|
|
2552
|
+
reason: input.reason,
|
|
2553
|
+
reconcile
|
|
2191
2554
|
};
|
|
2192
2555
|
}
|
|
2193
2556
|
function resolveDestroyConfirmation(context, expected, input) {
|
|
@@ -2211,8 +2574,8 @@ function resolveDestroyConfirmation(context, expected, input) {
|
|
|
2211
2574
|
function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
2212
2575
|
try {
|
|
2213
2576
|
if (remoteBranchExists(repoDir, branch)) {
|
|
2214
|
-
|
|
2215
|
-
|
|
2577
|
+
runGit(["pull", "--rebase", "origin", branch], { cwd: repoDir });
|
|
2578
|
+
runGit(["push", "origin", branch], { cwd: repoDir });
|
|
2216
2579
|
return {
|
|
2217
2580
|
remoteBranchExisted: true,
|
|
2218
2581
|
pulledRebase: true,
|
|
@@ -2221,7 +2584,7 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
|
2221
2584
|
conflicts: false
|
|
2222
2585
|
};
|
|
2223
2586
|
}
|
|
2224
|
-
|
|
2587
|
+
runGit(["push", "-u", "origin", branch], { cwd: repoDir });
|
|
2225
2588
|
return {
|
|
2226
2589
|
remoteBranchExisted: false,
|
|
2227
2590
|
pulledRebase: false,
|
|
@@ -2270,10 +2633,10 @@ function checkoutOrCreateSaveBranch(repoDir, branch) {
|
|
|
2270
2633
|
return branch;
|
|
2271
2634
|
}
|
|
2272
2635
|
if (remoteBranchExists(repoDir, branch)) {
|
|
2273
|
-
|
|
2636
|
+
runGit(["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
|
|
2274
2637
|
return branch;
|
|
2275
2638
|
}
|
|
2276
|
-
|
|
2639
|
+
runGit(["checkout", "-b", branch], { cwd: repoDir });
|
|
2277
2640
|
return branch;
|
|
2278
2641
|
}
|
|
2279
2642
|
function runPackageVerifyLocal(pkgDir) {
|
|
@@ -2283,8 +2646,8 @@ function branchNeedsSync(repoDir, branch) {
|
|
|
2283
2646
|
if (!remoteBranchExists(repoDir, branch)) {
|
|
2284
2647
|
return true;
|
|
2285
2648
|
}
|
|
2286
|
-
const localHead =
|
|
2287
|
-
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();
|
|
2288
2651
|
return localHead !== remoteHead;
|
|
2289
2652
|
}
|
|
2290
2653
|
function savePackageRepo(report, message, branch, shouldVerify) {
|
|
@@ -2294,7 +2657,7 @@ function savePackageRepo(report, message, branch, shouldVerify) {
|
|
|
2294
2657
|
const needsSync = branchNeedsSync(report.path, branch);
|
|
2295
2658
|
if (!report.dirty && !needsSync) {
|
|
2296
2659
|
report.skippedReason = "clean";
|
|
2297
|
-
report.commitSha =
|
|
2660
|
+
report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
2298
2661
|
return report;
|
|
2299
2662
|
}
|
|
2300
2663
|
if (shouldVerify && report.dirty) {
|
|
@@ -2302,11 +2665,11 @@ function savePackageRepo(report, message, branch, shouldVerify) {
|
|
|
2302
2665
|
report.verified = true;
|
|
2303
2666
|
}
|
|
2304
2667
|
if (report.dirty) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2668
|
+
runGit(["add", "-A"], { cwd: report.path });
|
|
2669
|
+
runGit(["commit", "-m", message], { cwd: report.path });
|
|
2307
2670
|
report.committed = true;
|
|
2308
2671
|
}
|
|
2309
|
-
report.commitSha =
|
|
2672
|
+
report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
2310
2673
|
const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
|
|
2311
2674
|
report.pushed = branchSync.pushed === true;
|
|
2312
2675
|
if (!report.dirty && needsSync) {
|
|
@@ -2344,14 +2707,14 @@ function ensureLocalTaskBranch(repoDir, branchName) {
|
|
|
2344
2707
|
}
|
|
2345
2708
|
return true;
|
|
2346
2709
|
}
|
|
2347
|
-
function cleanupTaskBranchReport(report, branchName,
|
|
2710
|
+
function cleanupTaskBranchReport(report, branchName, _message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
|
|
2348
2711
|
if (!ensureLocalTaskBranch(report.path, branchName)) {
|
|
2349
2712
|
report.skippedReason = "branch-missing";
|
|
2350
2713
|
return report;
|
|
2351
2714
|
}
|
|
2352
|
-
const
|
|
2353
|
-
report.tagName =
|
|
2354
|
-
report.commitSha =
|
|
2715
|
+
const tag = createDeprecatedTaskTag(report.path, branchName, _message);
|
|
2716
|
+
report.tagName = tag.tagName;
|
|
2717
|
+
report.commitSha = updateHead(report.path);
|
|
2355
2718
|
report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
|
|
2356
2719
|
syncBranchWithOrigin(report.path, targetBranch);
|
|
2357
2720
|
if (deleteBranch) {
|
|
@@ -2415,7 +2778,7 @@ function collectReleasePackageSelection(root) {
|
|
|
2415
2778
|
};
|
|
2416
2779
|
}
|
|
2417
2780
|
function hasStagedChanges(repoDir) {
|
|
2418
|
-
return
|
|
2781
|
+
return runGit(["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
|
|
2419
2782
|
}
|
|
2420
2783
|
async function workflowStatus(helpers, input = {}) {
|
|
2421
2784
|
return withContextEnv(helpers.context.env, async () => {
|
|
@@ -2837,7 +3200,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
2837
3200
|
const mode = session.mode;
|
|
2838
3201
|
const repoDir = session.gitRoot;
|
|
2839
3202
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
2840
|
-
const packageReports =
|
|
3203
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
2841
3204
|
let previewResult = null;
|
|
2842
3205
|
const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
2843
3206
|
if (executionMode === "plan") {
|
|
@@ -2847,6 +3210,14 @@ async function workflowSwitch(helpers, input) {
|
|
|
2847
3210
|
report.created = !local && !remote;
|
|
2848
3211
|
report.resumed = local || remote;
|
|
2849
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;
|
|
2850
3221
|
return buildWorkflowResult(
|
|
2851
3222
|
"switch",
|
|
2852
3223
|
root,
|
|
@@ -2864,7 +3235,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
2864
3235
|
...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
|
|
2865
3236
|
{ id: "workspace-link", description: "Apply local workspace links for integrated development" },
|
|
2866
3237
|
...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
|
|
2867
|
-
]
|
|
3238
|
+
],
|
|
3239
|
+
previewPlan
|
|
2868
3240
|
},
|
|
2869
3241
|
{
|
|
2870
3242
|
executionMode,
|
|
@@ -2914,8 +3286,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
2914
3286
|
rootRepo.resumed = rootSwitch.resumed;
|
|
2915
3287
|
rootRepo.commitSha = headCommit(repoDir);
|
|
2916
3288
|
rootRepo.pushed = rootSwitch.created;
|
|
2917
|
-
for (const
|
|
2918
|
-
const report = findReportByName(packageReports,
|
|
3289
|
+
for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
|
|
3290
|
+
const report = findReportByName(packageReports, managedRepo.name);
|
|
2919
3291
|
if (!report) {
|
|
2920
3292
|
continue;
|
|
2921
3293
|
}
|
|
@@ -2923,16 +3295,16 @@ async function workflowSwitch(helpers, input) {
|
|
|
2923
3295
|
root,
|
|
2924
3296
|
workflowRun.runId,
|
|
2925
3297
|
`switch-${report.name}`,
|
|
2926
|
-
() => checkoutTaskBranchFromStaging(
|
|
3298
|
+
() => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
|
|
2927
3299
|
createIfMissing: input.createIfMissing !== false,
|
|
2928
3300
|
pushIfCreated: false
|
|
2929
3301
|
})
|
|
2930
3302
|
);
|
|
2931
|
-
report.branch = currentBranch(
|
|
3303
|
+
report.branch = currentBranch(managedRepo.dir) || branchName;
|
|
2932
3304
|
report.created = packageSwitch.created;
|
|
2933
3305
|
report.resumed = packageSwitch.resumed;
|
|
2934
|
-
report.commitSha = headCommit(
|
|
2935
|
-
report.dirty = hasMeaningfulChanges(
|
|
3306
|
+
report.commitSha = headCommit(managedRepo.dir);
|
|
3307
|
+
report.dirty = hasMeaningfulChanges(managedRepo.dir);
|
|
2936
3308
|
}
|
|
2937
3309
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
|
|
2938
3310
|
const stateAfterSwitch = resolveTreeseedWorkflowState(root);
|
|
@@ -2941,7 +3313,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
2941
3313
|
root,
|
|
2942
3314
|
workflowRun.runId,
|
|
2943
3315
|
"preview",
|
|
2944
|
-
() =>
|
|
3316
|
+
() => reconcileWorkflowBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
|
|
2945
3317
|
) ?? null;
|
|
2946
3318
|
}
|
|
2947
3319
|
const state = resolveTreeseedWorkflowState(root);
|
|
@@ -2995,32 +3367,401 @@ async function workflowSwitch(helpers, input) {
|
|
|
2995
3367
|
toError("switch", error);
|
|
2996
3368
|
}
|
|
2997
3369
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
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" });
|
|
3750
|
+
const args = [packageScriptPath("tenant-dev")];
|
|
3751
|
+
if (input.watch) {
|
|
3752
|
+
args.push("--watch");
|
|
3753
|
+
}
|
|
3754
|
+
if (input.port !== void 0) {
|
|
3755
|
+
args.push("--port", String(input.port));
|
|
3756
|
+
}
|
|
3757
|
+
const env = resolveTreeseedLaunchEnvironment({
|
|
3758
|
+
tenantRoot,
|
|
3759
|
+
scope: "local",
|
|
3760
|
+
baseEnv: { ...process.env, ...helpers.context.env ?? {} }
|
|
3761
|
+
});
|
|
3762
|
+
if (input.background) {
|
|
3763
|
+
const child = spawn(process.execPath, args, {
|
|
3764
|
+
cwd: tenantRoot,
|
|
3024
3765
|
env,
|
|
3025
3766
|
stdio: input.stdio ?? "inherit",
|
|
3026
3767
|
detached: process.platform !== "win32"
|
|
@@ -3073,6 +3814,7 @@ async function workflowSave(helpers, input) {
|
|
|
3073
3814
|
return await withContextEnv(helpers.context.env, async () => {
|
|
3074
3815
|
const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
|
|
3075
3816
|
const root = workspaceRoot(tenantRoot);
|
|
3817
|
+
ensureTreeseedLocalStateExcluded(root);
|
|
3076
3818
|
const rootBranch = currentBranch(repoRoot(root)) || null;
|
|
3077
3819
|
reattachRepairablePackageRepos(root, [rootBranch, STAGING_BRANCH, PRODUCTION_BRANCH].filter((branch2) => Boolean(branch2)), {
|
|
3078
3820
|
operation: "save",
|
|
@@ -3104,7 +3846,7 @@ async function workflowSave(helpers, input) {
|
|
|
3104
3846
|
if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
|
|
3105
3847
|
workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
|
|
3106
3848
|
}
|
|
3107
|
-
const packageReports =
|
|
3849
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
3108
3850
|
const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
|
|
3109
3851
|
const blockers = [];
|
|
3110
3852
|
if (executionMode === "plan") {
|
|
@@ -3121,7 +3863,7 @@ async function workflowSave(helpers, input) {
|
|
|
3121
3863
|
message,
|
|
3122
3864
|
bump: effectiveInput.bump ?? "patch",
|
|
3123
3865
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
3124
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
3866
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
3125
3867
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3126
3868
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3127
3869
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
@@ -3160,8 +3902,9 @@ async function workflowSave(helpers, input) {
|
|
|
3160
3902
|
{ id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
|
|
3161
3903
|
...repositoryPlan.plannedSteps,
|
|
3162
3904
|
{ id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
|
|
3163
|
-
...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{ id: "hosted-ci", description: `Wait for hosted save workflows on ${branch}` }] : [],
|
|
3164
|
-
...
|
|
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` }] : [],
|
|
3165
3908
|
{ id: "workspace-link", description: "Restore local workspace links after save" },
|
|
3166
3909
|
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
3167
3910
|
]
|
|
@@ -3193,7 +3936,7 @@ async function workflowSave(helpers, input) {
|
|
|
3193
3936
|
verify: effectiveInput.verify !== false,
|
|
3194
3937
|
bump: effectiveInput.bump ?? "patch",
|
|
3195
3938
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
3196
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
3939
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
3197
3940
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3198
3941
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3199
3942
|
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
|
|
@@ -3216,13 +3959,21 @@ async function workflowSave(helpers, input) {
|
|
|
3216
3959
|
},
|
|
3217
3960
|
...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{
|
|
3218
3961
|
id: "hosted-ci",
|
|
3219
|
-
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}`,
|
|
3963
|
+
repoName: rootRepo.name,
|
|
3964
|
+
repoPath: rootRepo.path,
|
|
3965
|
+
branch,
|
|
3966
|
+
resumable: true
|
|
3967
|
+
}] : [],
|
|
3968
|
+
...saveLane === "promotion" ? [{
|
|
3969
|
+
id: "release-proof",
|
|
3970
|
+
description: "Run authoritative hosted release proof records",
|
|
3220
3971
|
repoName: rootRepo.name,
|
|
3221
3972
|
repoPath: rootRepo.path,
|
|
3222
3973
|
branch,
|
|
3223
3974
|
resumable: true
|
|
3224
3975
|
}] : [],
|
|
3225
|
-
...branch === STAGING_BRANCH ? [{
|
|
3976
|
+
...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{
|
|
3226
3977
|
id: "release-candidate",
|
|
3227
3978
|
description: "Run release-candidate readiness checks",
|
|
3228
3979
|
repoName: rootRepo.name,
|
|
@@ -3263,19 +4014,21 @@ async function workflowSave(helpers, input) {
|
|
|
3263
4014
|
message,
|
|
3264
4015
|
bump: effectiveInput.bump ?? "patch",
|
|
3265
4016
|
devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
|
|
3266
|
-
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-
|
|
4017
|
+
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
|
|
3267
4018
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3268
4019
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3269
4020
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
3270
4021
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
3271
4022
|
workflowRunId: workflowRun.runId,
|
|
4023
|
+
deferPushUntilVerified: branch === STAGING_BRANCH,
|
|
3272
4024
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
3273
4025
|
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
3274
4026
|
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
3275
4027
|
const rootReportForWave = nodes.some((node) => node.id === ".") ? waveRootRepo : null;
|
|
4028
|
+
const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
|
|
3276
4029
|
const gates = [
|
|
3277
4030
|
...gatesForSavedRepositoryReports(root, nonRootReportsForWave),
|
|
3278
|
-
...rootReportForWave ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
|
|
4031
|
+
...rootReportForWave && !hostedEnvironment ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
|
|
3279
4032
|
];
|
|
3280
4033
|
if (gates.length === 0) {
|
|
3281
4034
|
return [];
|
|
@@ -3283,7 +4036,7 @@ async function workflowSave(helpers, input) {
|
|
|
3283
4036
|
const repositoryNames = gates.map((gate) => gate.name).join(", ");
|
|
3284
4037
|
if (nonRootReportsForWave.length > 0) {
|
|
3285
4038
|
helpers.write(`[save][workflow] Waiting for hosted repository gates before saving dependents: ${repositoryNames}.`);
|
|
3286
|
-
} else if (rootReportForWave) {
|
|
4039
|
+
} else if (rootReportForWave && !hostedEnvironment) {
|
|
3287
4040
|
helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
|
|
3288
4041
|
}
|
|
3289
4042
|
return waitForWorkflowGates("save", gates, "hosted", {
|
|
@@ -3300,7 +4053,7 @@ async function workflowSave(helpers, input) {
|
|
|
3300
4053
|
const savedPackageReports = saveResult?.repos ?? packageReports;
|
|
3301
4054
|
const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
|
|
3302
4055
|
helpers.write("[save][workflow] Repository save phase complete; checking command readiness.");
|
|
3303
|
-
const head = savedRootRepo.commitSha ??
|
|
4056
|
+
const head = savedRootRepo.commitSha ?? runGit(["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
3304
4057
|
const commitCreated = savedRootRepo.committed === true;
|
|
3305
4058
|
const branchSync = {
|
|
3306
4059
|
...savedRootRepo.publishWait ?? {},
|
|
@@ -3317,42 +4070,12 @@ async function workflowSave(helpers, input) {
|
|
|
3317
4070
|
}))
|
|
3318
4071
|
};
|
|
3319
4072
|
const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
|
|
3320
|
-
|
|
4073
|
+
const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
|
|
4074
|
+
if (hostedEnvironment) {
|
|
3321
4075
|
const workflowGates = saveResult?.workflowGates ?? [];
|
|
3322
|
-
if (effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
|
|
3323
|
-
return { workflowGates };
|
|
3324
|
-
}
|
|
3325
|
-
helpers.write("[save][workflow] Dispatching hosted market deploy gate for deployed resource verification.");
|
|
3326
|
-
const repository = resolveGitHubRepositorySlug(savedRootRepo.path);
|
|
3327
|
-
await dispatchGitHubWorkflowRun(repository, {
|
|
3328
|
-
workflow: "deploy.yml",
|
|
3329
|
-
branch,
|
|
3330
|
-
inputs: {
|
|
3331
|
-
environment: "staging",
|
|
3332
|
-
action_kind: "deploy_web"
|
|
3333
|
-
}
|
|
3334
|
-
});
|
|
3335
|
-
await sleep(5e3);
|
|
3336
|
-
helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
|
|
3337
|
-
const dispatchedGates = await waitForWorkflowGates("save", [
|
|
3338
|
-
hostedDeployGate({
|
|
3339
|
-
name: savedRootRepo.name,
|
|
3340
|
-
repoPath: savedRootRepo.path,
|
|
3341
|
-
repository,
|
|
3342
|
-
workflow: "deploy.yml",
|
|
3343
|
-
branch,
|
|
3344
|
-
headSha: savedRootRepo.commitSha
|
|
3345
|
-
})
|
|
3346
|
-
], "hosted", {
|
|
3347
|
-
root,
|
|
3348
|
-
runId: workflowRun.runId,
|
|
3349
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3350
|
-
});
|
|
3351
4076
|
return {
|
|
3352
|
-
workflowGates
|
|
3353
|
-
|
|
3354
|
-
...dispatchedGates
|
|
3355
|
-
]
|
|
4077
|
+
workflowGates,
|
|
4078
|
+
hostedReconcile: await reconcileSaveHostedEnvironment(root, hostedEnvironment, helpers, workflowRun.runId)
|
|
3356
4079
|
};
|
|
3357
4080
|
}
|
|
3358
4081
|
helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
|
|
@@ -3371,21 +4094,44 @@ async function workflowSave(helpers, input) {
|
|
|
3371
4094
|
branch,
|
|
3372
4095
|
headSha: savedRootRepo.commitSha
|
|
3373
4096
|
})] : [],
|
|
3374
|
-
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) =>
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
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
|
+
})
|
|
3381
4108
|
], "hosted", {
|
|
3382
4109
|
root,
|
|
3383
4110
|
runId: workflowRun.runId,
|
|
3384
4111
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3385
4112
|
}).then((workflowGates) => ({ workflowGates }));
|
|
3386
4113
|
}) : { workflowGates: [] };
|
|
3387
|
-
const
|
|
3388
|
-
helpers.write(
|
|
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}).`);
|
|
3389
4135
|
const releaseSession = resolveTreeseedWorkflowSession(root);
|
|
3390
4136
|
const stagingReleasePlan = buildReleasePlanSnapshot({
|
|
3391
4137
|
root,
|
|
@@ -3396,19 +4142,23 @@ async function workflowSave(helpers, input) {
|
|
|
3396
4142
|
rootRepo: savedRootRepo,
|
|
3397
4143
|
blockers: []
|
|
3398
4144
|
});
|
|
3399
|
-
return
|
|
4145
|
+
return runReleaseCandidateProofForPlan("save", root, stagingReleasePlan, {
|
|
4146
|
+
mode: releaseCandidateMode,
|
|
4147
|
+
lane: saveLane,
|
|
4148
|
+
write: (line, stream) => helpers.write(line, stream)
|
|
4149
|
+
});
|
|
3400
4150
|
}) : null;
|
|
3401
4151
|
let previewAction = { status: "skipped" };
|
|
3402
4152
|
if (beforeState.branchRole === "feature" && branch) {
|
|
3403
4153
|
if (effectiveInput.preview === true) {
|
|
3404
4154
|
previewAction = {
|
|
3405
4155
|
status: previewInitialized ? "refreshed" : "created",
|
|
3406
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () =>
|
|
4156
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
|
|
3407
4157
|
};
|
|
3408
4158
|
} else if (effectiveInput.refreshPreview !== false && previewInitialized) {
|
|
3409
4159
|
previewAction = {
|
|
3410
4160
|
status: "refreshed",
|
|
3411
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () =>
|
|
4161
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
3412
4162
|
};
|
|
3413
4163
|
}
|
|
3414
4164
|
}
|
|
@@ -3417,7 +4167,7 @@ async function workflowSave(helpers, input) {
|
|
|
3417
4167
|
selected: savedPackageReports.map((report) => report.name)
|
|
3418
4168
|
}
|
|
3419
4169
|
});
|
|
3420
|
-
const hostingAudit = await
|
|
4170
|
+
const hostingAudit = await workflowHostedVerificationGateRequired(
|
|
3421
4171
|
"save",
|
|
3422
4172
|
root,
|
|
3423
4173
|
helpers,
|
|
@@ -3458,7 +4208,9 @@ async function workflowSave(helpers, input) {
|
|
|
3458
4208
|
releaseCandidateMode,
|
|
3459
4209
|
applicationSelection,
|
|
3460
4210
|
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
4211
|
+
hostedReconcile: saveWorkflowGates?.hostedReconcile ?? null,
|
|
3461
4212
|
releaseCandidate,
|
|
4213
|
+
releaseProof,
|
|
3462
4214
|
hostingAudit,
|
|
3463
4215
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3464
4216
|
};
|
|
@@ -3540,17 +4292,17 @@ async function workflowClose(helpers, input) {
|
|
|
3540
4292
|
failure: planAutoResumeRun.failure
|
|
3541
4293
|
} : null,
|
|
3542
4294
|
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3543
|
-
autoSaveRequired: session.rootRepo.dirty || session.
|
|
3544
|
-
repos:
|
|
4295
|
+
autoSaveRequired: session.rootRepo.dirty || session.managedRepos.some((repo) => repo.dirty),
|
|
4296
|
+
repos: createManagedWorkflowRepoReports(root),
|
|
3545
4297
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
3546
4298
|
blockers,
|
|
3547
4299
|
plannedSteps: [
|
|
3548
4300
|
{ id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
|
|
3549
4301
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
3550
4302
|
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
3551
|
-
...
|
|
3552
|
-
id: `cleanup-${
|
|
3553
|
-
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}`
|
|
3554
4306
|
})),
|
|
3555
4307
|
{ id: "workspace-link", description: "Restore local workspace links on the final branch" },
|
|
3556
4308
|
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
|
|
@@ -3572,6 +4324,7 @@ async function workflowClose(helpers, input) {
|
|
|
3572
4324
|
const featureBranch = assertFeatureBranch(root);
|
|
3573
4325
|
const mode = activeSession.mode;
|
|
3574
4326
|
const repoDir = activeSession.gitRoot;
|
|
4327
|
+
const managedWorktreeForClose = isManagedWorkflowWorktree(root);
|
|
3575
4328
|
assertSessionBranchSafety("close", activeSession);
|
|
3576
4329
|
if (mode === "recursive-workspace") {
|
|
3577
4330
|
assertWorkspaceClean(root);
|
|
@@ -3579,7 +4332,7 @@ async function workflowClose(helpers, input) {
|
|
|
3579
4332
|
assertCleanWorktree(root);
|
|
3580
4333
|
}
|
|
3581
4334
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3582
|
-
const packageReports =
|
|
4335
|
+
const packageReports = createManagedWorkflowRepoReports(root);
|
|
3583
4336
|
const workflowRun = acquireWorkflowRun(
|
|
3584
4337
|
"close",
|
|
3585
4338
|
activeSession,
|
|
@@ -3617,30 +4370,34 @@ async function workflowClose(helpers, input) {
|
|
|
3617
4370
|
}
|
|
3618
4371
|
try {
|
|
3619
4372
|
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
3620
|
-
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));
|
|
3621
4374
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
3622
|
-
const
|
|
4375
|
+
const head = updateHead(repoDir);
|
|
4376
|
+
const tag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
3623
4377
|
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
3624
|
-
|
|
3625
|
-
|
|
4378
|
+
if (!managedWorktreeForClose) {
|
|
4379
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
4380
|
+
}
|
|
4381
|
+
if (effectiveInput.deleteBranch !== false && !managedWorktreeForClose) {
|
|
3626
4382
|
deleteLocalBranch(repoDir, featureBranch);
|
|
3627
4383
|
}
|
|
3628
4384
|
return {
|
|
3629
|
-
|
|
4385
|
+
head,
|
|
4386
|
+
tagName: tag.tagName,
|
|
3630
4387
|
deletedRemote,
|
|
3631
|
-
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
3632
|
-
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
4388
|
+
deletedLocal: effectiveInput.deleteBranch !== false && !managedWorktreeForClose,
|
|
4389
|
+
branch: managedWorktreeForClose ? featureBranch : currentBranch(repoDir) || STAGING_BRANCH,
|
|
3633
4390
|
dirty: hasMeaningfulChanges(repoDir)
|
|
3634
4391
|
};
|
|
3635
4392
|
});
|
|
3636
|
-
rootRepo.tagName =
|
|
3637
|
-
rootRepo.commitSha = String(rootCleanup?.
|
|
4393
|
+
rootRepo.tagName = typeof rootCleanup?.tagName === "string" ? rootCleanup.tagName : null;
|
|
4394
|
+
rootRepo.commitSha = String(rootCleanup?.head ?? rootRepo.commitSha ?? "");
|
|
3638
4395
|
rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
|
|
3639
4396
|
rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
|
|
3640
4397
|
rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3641
4398
|
rootRepo.dirty = rootCleanup?.dirty === true;
|
|
3642
|
-
for (const
|
|
3643
|
-
const report = findReportByName(packageReports,
|
|
4399
|
+
for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
|
|
4400
|
+
const report = findReportByName(packageReports, managedRepo.name);
|
|
3644
4401
|
if (!report) {
|
|
3645
4402
|
continue;
|
|
3646
4403
|
}
|
|
@@ -3651,16 +4408,21 @@ async function workflowClose(helpers, input) {
|
|
|
3651
4408
|
Object.assign(report, cleanup);
|
|
3652
4409
|
}
|
|
3653
4410
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
3654
|
-
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
4411
|
+
const finalBranch = managedWorktreeForClose ? STAGING_BRANCH : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3655
4412
|
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
3656
|
-
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
|
+
}
|
|
3657
4420
|
const payload = {
|
|
3658
4421
|
mode,
|
|
3659
4422
|
branchName: featureBranch,
|
|
3660
4423
|
message,
|
|
3661
4424
|
autoSaved: autoSave.performed,
|
|
3662
4425
|
autoSaveResult: autoSave.save,
|
|
3663
|
-
deprecatedTag: rootCleanup?.deprecatedTag ?? null,
|
|
3664
4426
|
repos: packageReports,
|
|
3665
4427
|
rootRepo,
|
|
3666
4428
|
previewCleanup,
|
|
@@ -3681,6 +4443,7 @@ async function workflowClose(helpers, input) {
|
|
|
3681
4443
|
payload,
|
|
3682
4444
|
{
|
|
3683
4445
|
runId: workflowRun.runId,
|
|
4446
|
+
includeFinalState: !managedWorktreeForClose,
|
|
3684
4447
|
nextSteps: createNextSteps([
|
|
3685
4448
|
{ operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
|
|
3686
4449
|
])
|
|
@@ -3703,371 +4466,460 @@ async function workflowClose(helpers, input) {
|
|
|
3703
4466
|
toError("close", error);
|
|
3704
4467
|
}
|
|
3705
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
|
+
}
|
|
3706
4659
|
async function workflowStage(helpers, input) {
|
|
3707
4660
|
try {
|
|
3708
4661
|
return await withContextEnv(helpers.context.env, async () => {
|
|
3709
4662
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
3710
4663
|
const root = workspaceRoot(tenantRoot);
|
|
3711
4664
|
const executionMode = normalizeExecutionMode(input);
|
|
3712
|
-
const
|
|
4665
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3713
4666
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3714
|
-
const
|
|
3715
|
-
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;
|
|
3716
4670
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
3717
4671
|
const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
if (executionMode === "plan") {
|
|
3721
|
-
const blockers = [];
|
|
3722
|
-
if (initialSession.branchRole !== "feature") {
|
|
3723
|
-
blockers.push("Stage only applies to task branches.");
|
|
3724
|
-
}
|
|
3725
|
-
try {
|
|
3726
|
-
validateStagingWorkflowContracts(root);
|
|
3727
|
-
} catch (error) {
|
|
3728
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
3729
|
-
}
|
|
3730
|
-
const applicationSelection = selectWorkflowApplications(root, { packageSelection: initialSession.packageSelection });
|
|
3731
|
-
const readiness = collectTreeseedDeploymentReadiness({
|
|
3732
|
-
tenantRoot: root,
|
|
3733
|
-
environment: "staging",
|
|
3734
|
-
appId: singleSelectedWorkflowAppId(applicationSelection)
|
|
3735
|
-
});
|
|
3736
|
-
blockers.push(...readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
3737
|
-
return buildWorkflowResult(
|
|
3738
|
-
"stage",
|
|
3739
|
-
root,
|
|
3740
|
-
{
|
|
3741
|
-
mode: initialSession.mode,
|
|
3742
|
-
branchName: initialSession.branchName,
|
|
3743
|
-
mergeTarget: STAGING_BRANCH,
|
|
3744
|
-
mergeStrategy: "squash",
|
|
3745
|
-
message,
|
|
3746
|
-
ciMode,
|
|
3747
|
-
applicationSelection,
|
|
3748
|
-
autoResumeCandidate: planAutoResumeRun ? {
|
|
3749
|
-
runId: planAutoResumeRun.runId,
|
|
3750
|
-
branch: planAutoResumeRun.session.branchName,
|
|
3751
|
-
failure: planAutoResumeRun.failure
|
|
3752
|
-
} : null,
|
|
3753
|
-
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3754
|
-
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
3755
|
-
blockers,
|
|
3756
|
-
readiness,
|
|
3757
|
-
rootRepo: createWorkspaceRootRepoReport(root),
|
|
3758
|
-
repos: createWorkspacePackageReports(root),
|
|
3759
|
-
plannedSteps: [
|
|
3760
|
-
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
3761
|
-
id: `merge-${pkg.name}`,
|
|
3762
|
-
description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
|
|
3763
|
-
})),
|
|
3764
|
-
{ id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
|
|
3765
|
-
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
3766
|
-
{ id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
|
|
3767
|
-
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
|
|
3768
|
-
{ id: "release-candidate", description: "Run release-candidate readiness checks for the exact staging state" },
|
|
3769
|
-
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
3770
|
-
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
3771
|
-
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
3772
|
-
id: `cleanup-${pkg.name}`,
|
|
3773
|
-
description: `Archive and delete the task branch from ${pkg.name}`
|
|
3774
|
-
})),
|
|
3775
|
-
{ id: "workspace-link", description: "Restore local workspace links on staging" }
|
|
3776
|
-
]
|
|
3777
|
-
},
|
|
3778
|
-
{
|
|
3779
|
-
executionMode,
|
|
3780
|
-
nextSteps: createNextSteps([
|
|
3781
|
-
{ operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
|
|
3782
|
-
])
|
|
3783
|
-
}
|
|
3784
|
-
);
|
|
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.");
|
|
3785
4674
|
}
|
|
3786
|
-
const
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
const mode = session.mode;
|
|
3793
|
-
assertSessionBranchSafety("stage", session);
|
|
3794
|
-
if (mode === "recursive-workspace") {
|
|
3795
|
-
assertWorkspaceClean(root);
|
|
3796
|
-
} else {
|
|
3797
|
-
assertCleanWorktree(root);
|
|
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}.`);
|
|
3798
4681
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
const
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
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
|
+
};
|
|
4710
|
+
if (executionMode === "plan") {
|
|
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
|
+
});
|
|
3812
4719
|
}
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
const packageReports = createWorkspacePackageReports(root);
|
|
3816
|
-
const workflowRun = acquireWorkflowRun(
|
|
3817
|
-
"stage",
|
|
3818
|
-
session,
|
|
3819
|
-
{
|
|
4720
|
+
if (effectiveInput.autoSave === true) {
|
|
4721
|
+
await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
3820
4722
|
message,
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
repoName: report.name,
|
|
3834
|
-
repoPath: report.path,
|
|
3835
|
-
branch: featureBranch,
|
|
3836
|
-
resumable: true
|
|
3837
|
-
})),
|
|
3838
|
-
{ id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3839
|
-
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3840
|
-
{ id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3841
|
-
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3842
|
-
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
3843
|
-
...packageReports.map((report) => ({
|
|
3844
|
-
id: `cleanup-${report.name}`,
|
|
3845
|
-
description: `Archive ${featureBranch} in ${report.name}`,
|
|
3846
|
-
repoName: report.name,
|
|
3847
|
-
repoPath: report.path,
|
|
3848
|
-
branch: featureBranch,
|
|
3849
|
-
resumable: true
|
|
3850
|
-
})),
|
|
3851
|
-
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3852
|
-
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
3853
|
-
],
|
|
3854
|
-
autoResumeRun ? {
|
|
3855
|
-
...helpers.context,
|
|
3856
|
-
workflow: {
|
|
3857
|
-
...helpers.context.workflow ?? {},
|
|
3858
|
-
resumeRunId: autoResumeRun.runId
|
|
3859
|
-
}
|
|
3860
|
-
} : helpers.context
|
|
3861
|
-
);
|
|
3862
|
-
if (autoResumeRun) {
|
|
3863
|
-
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);
|
|
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
|
+
});
|
|
3864
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);
|
|
3865
4754
|
try {
|
|
3866
|
-
await executeJournalStep(root, workflowRun.runId, "
|
|
3867
|
-
|
|
3868
|
-
const
|
|
3869
|
-
if (
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
|
|
3875
|
-
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
|
+
});
|
|
3876
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 = [];
|
|
3877
4768
|
try {
|
|
3878
|
-
const
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
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
|
+
});
|
|
3886
4795
|
} catch (error) {
|
|
3887
|
-
const
|
|
3888
|
-
throw
|
|
3889
|
-
details
|
|
3890
|
-
|
|
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
|
|
3891
4802
|
});
|
|
3892
4803
|
}
|
|
3893
|
-
|
|
3894
|
-
let rootMerge = null;
|
|
3895
|
-
try {
|
|
3896
|
-
rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
|
|
3897
|
-
assertCleanWorktree(root);
|
|
3898
|
-
if (isManagedWorkflowWorktree(root)) {
|
|
3899
|
-
checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
|
|
3900
|
-
} else {
|
|
3901
|
-
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
3902
|
-
}
|
|
3903
|
-
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
3904
|
-
if (mode === "recursive-workspace") {
|
|
3905
|
-
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
3906
|
-
}
|
|
3907
|
-
const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
|
|
3908
|
-
root,
|
|
3909
|
-
gitRoot: repoDir,
|
|
3910
|
-
branch: STAGING_BRANCH,
|
|
3911
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3912
|
-
});
|
|
3913
|
-
if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
|
|
3914
|
-
run("git", ["add", "-A"], { cwd: repoDir });
|
|
3915
|
-
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
3916
|
-
}
|
|
3917
|
-
if (isManagedWorkflowWorktree(root)) {
|
|
3918
|
-
pushHeadToBranch(repoDir, STAGING_BRANCH);
|
|
3919
|
-
} else {
|
|
3920
|
-
pushBranch(repoDir, STAGING_BRANCH);
|
|
3921
|
-
}
|
|
3922
|
-
return {
|
|
3923
|
-
commitSha: headCommit(repoDir),
|
|
3924
|
-
branch: STAGING_BRANCH,
|
|
3925
|
-
committed: hasMeaningfulChanges(repoDir) ? false : true,
|
|
3926
|
-
lockfileValidation: lockfileSafety.lockfileValidation,
|
|
3927
|
-
lockfileInstall: lockfileSafety.install
|
|
3928
|
-
};
|
|
3929
|
-
});
|
|
3930
|
-
rootRepo.merged = true;
|
|
3931
|
-
rootRepo.committed = true;
|
|
3932
|
-
rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
|
|
3933
|
-
rootRepo.pushed = true;
|
|
3934
|
-
rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
3935
|
-
} catch (error) {
|
|
3936
|
-
const report = collectMergeConflictReport(repoDir);
|
|
3937
|
-
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
3938
|
-
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
3939
|
-
exitCode: 12
|
|
3940
|
-
});
|
|
3941
|
-
}
|
|
3942
|
-
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", [
|
|
3943
|
-
hostedDeployGate({
|
|
3944
|
-
name: rootRepo.name,
|
|
3945
|
-
repoPath: rootRepo.path,
|
|
3946
|
-
workflow: "deploy.yml",
|
|
3947
|
-
branch: STAGING_BRANCH,
|
|
3948
|
-
headSha: rootRepo.commitSha
|
|
3949
|
-
}),
|
|
3950
|
-
...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
|
|
3951
|
-
name: report.name,
|
|
3952
|
-
repoPath: report.path,
|
|
3953
|
-
workflow: "verify.yml",
|
|
3954
|
-
branch: STAGING_BRANCH,
|
|
3955
|
-
headSha: String(report.commitSha)
|
|
3956
|
-
}))
|
|
3957
|
-
], ciMode, {
|
|
3958
|
-
root,
|
|
3959
|
-
runId: workflowRun.runId,
|
|
3960
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3961
|
-
}).then((workflowGates) => ({
|
|
3962
|
-
status: "completed",
|
|
3963
|
-
workflowGates
|
|
3964
|
-
})));
|
|
3965
|
-
const stagingWait = {
|
|
3966
|
-
status: String(stageWorkflowGateResult?.status ?? "completed"),
|
|
3967
|
-
workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
|
|
3968
|
-
};
|
|
3969
|
-
const stageReleasePlan = buildReleasePlanSnapshot({
|
|
3970
|
-
root,
|
|
3971
|
-
mode,
|
|
3972
|
-
level: "patch",
|
|
3973
|
-
packageSelection: session.packageSelection,
|
|
3974
|
-
packageReports,
|
|
3975
|
-
rootRepo,
|
|
3976
|
-
blockers: []
|
|
4804
|
+
return { status: "completed", results };
|
|
3977
4805
|
});
|
|
3978
|
-
const
|
|
3979
|
-
const
|
|
3980
|
-
|
|
3981
|
-
"
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
)
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
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
|
+
});
|
|
3998
4835
|
}
|
|
3999
4836
|
return {
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4837
|
+
mode: verifyMode,
|
|
4838
|
+
status: "passed",
|
|
4839
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4840
|
+
proof
|
|
4004
4841
|
};
|
|
4005
4842
|
});
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
const report = findReportByName(packageReports, pkg.name);
|
|
4013
|
-
if (!report) {
|
|
4014
|
-
continue;
|
|
4015
|
-
}
|
|
4016
|
-
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
4017
|
-
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
4018
|
-
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
|
|
4019
4849
|
}));
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
const
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
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" });
|
|
4026
4893
|
const payload = {
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
releaseCandidate,
|
|
4039
|
-
applicationSelection,
|
|
4040
|
-
previewCleanup,
|
|
4041
|
-
lockfileValidation: rootMerge?.lockfileValidation ?? null,
|
|
4042
|
-
lockfileInstall: rootMerge?.lockfileInstall ?? null,
|
|
4043
|
-
remoteDeleted: rootRepo.deletedRemote,
|
|
4044
|
-
localDeleted: rootRepo.deletedLocal,
|
|
4045
|
-
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,
|
|
4046
4905
|
workspaceLinks,
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
hostingAudit,
|
|
4050
|
-
worktreeCleanup,
|
|
4051
|
-
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
4052
|
-
managedWorktree,
|
|
4053
|
-
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
4054
|
-
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."
|
|
4055
4908
|
};
|
|
4056
4909
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
4057
|
-
return buildWorkflowResult(
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
{ operation: "status", reason: "Inspect staging readiness after the task branch merge." }
|
|
4066
|
-
])
|
|
4067
|
-
}
|
|
4068
|
-
);
|
|
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
|
+
});
|
|
4069
4918
|
} catch (error) {
|
|
4070
|
-
|
|
4919
|
+
try {
|
|
4920
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4921
|
+
} catch {
|
|
4922
|
+
}
|
|
4071
4923
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
4072
4924
|
resumable: true,
|
|
4073
4925
|
runId: workflowRun.runId,
|
|
@@ -4083,44 +4935,97 @@ ${stagingReadiness.checks.filter((check) => check.status === "failed").map((chec
|
|
|
4083
4935
|
toError("stage", error);
|
|
4084
4936
|
}
|
|
4085
4937
|
}
|
|
4086
|
-
async function
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
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:")) || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
|
|
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
|
+
}
|
|
4100
4977
|
});
|
|
4101
|
-
} catch (error) {
|
|
4102
|
-
toError("tags:cleanup", error);
|
|
4103
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
|
+
});
|
|
4104
5018
|
}
|
|
4105
5019
|
async function workflowRelease(helpers, input) {
|
|
4106
5020
|
try {
|
|
4107
5021
|
return await withContextEnv(helpers.context.env, async () => {
|
|
4108
|
-
const root = resolveProjectRootOrThrow("release", helpers.cwd());
|
|
4109
|
-
reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
|
|
4110
|
-
operation: "release",
|
|
4111
|
-
onProgress: (line, stream) => helpers.write(line, stream),
|
|
4112
|
-
throwOnBlocker: true
|
|
4113
|
-
});
|
|
5022
|
+
const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
|
|
4114
5023
|
const session = resolveTreeseedWorkflowSession(root);
|
|
4115
|
-
const gitRoot = session.gitRoot;
|
|
4116
|
-
const mode = session.mode;
|
|
4117
5024
|
const executionMode = normalizeExecutionMode(input);
|
|
4118
5025
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
4119
5026
|
const packageReports = createWorkspacePackageReports(root);
|
|
4120
5027
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
4121
|
-
const
|
|
4122
|
-
const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
4123
|
-
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;
|
|
4124
5029
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
4125
5030
|
const effectiveInput = autoResumeRun ? {
|
|
4126
5031
|
...autoResumeRun.input,
|
|
@@ -4128,11 +5033,10 @@ async function workflowRelease(helpers, input) {
|
|
|
4128
5033
|
} : input;
|
|
4129
5034
|
const level = effectiveInput.bump ?? "patch";
|
|
4130
5035
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
4131
|
-
const isResume = Boolean(explicitResumeRunId || autoResumeRun);
|
|
4132
5036
|
const packageSelection = session.packageSelection;
|
|
4133
5037
|
const plannedRelease = buildReleasePlanSnapshot({
|
|
4134
5038
|
root,
|
|
4135
|
-
mode,
|
|
5039
|
+
mode: session.mode,
|
|
4136
5040
|
level,
|
|
4137
5041
|
repairVersionLine: effectiveInput.repairVersionLine === true,
|
|
4138
5042
|
targetVersionLine: effectiveInput.targetVersionLine,
|
|
@@ -4141,9 +5045,8 @@ async function workflowRelease(helpers, input) {
|
|
|
4141
5045
|
rootRepo,
|
|
4142
5046
|
blockers: []
|
|
4143
5047
|
});
|
|
4144
|
-
const
|
|
4145
|
-
const
|
|
4146
|
-
const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, plannedPackageSelection.selected, {
|
|
5048
|
+
const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
|
|
5049
|
+
const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
4147
5050
|
level,
|
|
4148
5051
|
repairVersionLine: effectiveInput.repairVersionLine === true
|
|
4149
5052
|
});
|
|
@@ -4152,68 +5055,81 @@ async function workflowRelease(helpers, input) {
|
|
|
4152
5055
|
environment: "prod",
|
|
4153
5056
|
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
4154
5057
|
});
|
|
4155
|
-
|
|
4156
|
-
blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
4157
|
-
}
|
|
5058
|
+
blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
4158
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
|
+
};
|
|
4159
5074
|
if (executionMode === "plan") {
|
|
4160
|
-
return
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
} : null
|
|
4172
|
-
}, {
|
|
4173
|
-
executionMode,
|
|
4174
|
-
nextSteps: createNextSteps([
|
|
4175
|
-
{ operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
|
|
4176
|
-
])
|
|
4177
|
-
});
|
|
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
|
+
);
|
|
4178
5086
|
}
|
|
4179
5087
|
if (blockers.length > 0) {
|
|
4180
|
-
workflowError("release", "validation_failed", blockers
|
|
4181
|
-
|
|
5088
|
+
workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
|
|
5089
|
+
${blockers.join("\n")}`, {
|
|
5090
|
+
details: { blockers, releasePlan: plannedRelease }
|
|
4182
5091
|
});
|
|
4183
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);
|
|
4184
5104
|
const workflowRun = acquireWorkflowRun(
|
|
4185
5105
|
"release",
|
|
4186
5106
|
session,
|
|
4187
5107
|
{
|
|
5108
|
+
...effectiveInput,
|
|
4188
5109
|
bump: level,
|
|
4189
|
-
repairVersionLine: effectiveInput.repairVersionLine === true,
|
|
4190
|
-
targetVersionLine: effectiveInput.targetVersionLine,
|
|
4191
|
-
devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
|
|
4192
|
-
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
4193
|
-
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
4194
5110
|
ciMode,
|
|
4195
|
-
fresh: input.fresh === true
|
|
4196
|
-
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
4197
|
-
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
5111
|
+
fresh: input.fresh === true
|
|
4198
5112
|
},
|
|
4199
5113
|
[
|
|
4200
|
-
{ id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
4201
|
-
{ id: "release-
|
|
4202
|
-
{ id: "
|
|
4203
|
-
{ id: "
|
|
4204
|
-
...
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
{ id: "release-
|
|
4216
|
-
|
|
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 }
|
|
4217
5133
|
],
|
|
4218
5134
|
autoResumeRun ? {
|
|
4219
5135
|
...helpers.context,
|
|
@@ -4224,565 +5140,188 @@ async function workflowRelease(helpers, input) {
|
|
|
4224
5140
|
} : helpers.context
|
|
4225
5141
|
);
|
|
4226
5142
|
if (autoResumeRun) {
|
|
4227
|
-
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}
|
|
4228
|
-
}
|
|
4229
|
-
const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
|
|
4230
|
-
if (resumeAtRootGates) {
|
|
4231
|
-
helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
|
|
5143
|
+
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}.`);
|
|
4232
5144
|
}
|
|
4233
|
-
let releaseCleanupSnapshot = null;
|
|
4234
5145
|
try {
|
|
4235
|
-
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () =>
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
const
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
);
|
|
4256
|
-
|
|
4257
|
-
const stagingGateResult = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-staging-gates") : await executeJournalStep(root, workflowRun.runId, "release-staging-gates", () => {
|
|
4258
|
-
helpers.write("[release][workflow] Verifying current staging gates before production release.");
|
|
4259
|
-
const packageGates = checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => ({
|
|
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) => ({
|
|
4260
5168
|
name: pkg.name,
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
branch: STAGING_BRANCH,
|
|
4264
|
-
headSha: headCommit(pkg.dir)
|
|
4265
|
-
}));
|
|
4266
|
-
return waitForWorkflowGates("release", [
|
|
4267
|
-
hostedDeployGate({
|
|
4268
|
-
name: rootRepo.name,
|
|
4269
|
-
repoPath: rootRepo.path,
|
|
4270
|
-
workflow: "deploy.yml",
|
|
4271
|
-
branch: STAGING_BRANCH,
|
|
4272
|
-
headSha: headCommit(gitRoot)
|
|
4273
|
-
}),
|
|
4274
|
-
...packageGates
|
|
4275
|
-
], ciMode, {
|
|
4276
|
-
root,
|
|
4277
|
-
runId: workflowRun.runId,
|
|
4278
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4279
|
-
}).then((workflowGates) => ({ workflowGates }));
|
|
4280
|
-
});
|
|
4281
|
-
const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
|
|
4282
|
-
if (!resumeAtRootGates && !isResume) {
|
|
4283
|
-
assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
|
|
4284
|
-
assertCleanWorktree(root);
|
|
4285
|
-
}
|
|
4286
|
-
if (!resumeAtRootGates) {
|
|
4287
|
-
prepareReleaseBranches(root);
|
|
4288
|
-
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4289
|
-
runWorkspaceReleasePreflight({ cwd: root });
|
|
4290
|
-
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
4291
|
-
}
|
|
4292
|
-
if (mode === "root-only") {
|
|
4293
|
-
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
4294
|
-
setRootPackageJsonVersion(root, rootVersion);
|
|
4295
|
-
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
4296
|
-
const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
|
|
4297
|
-
const changelog = updateReleaseChangelog(gitRoot, {
|
|
4298
|
-
version: rootVersion,
|
|
4299
|
-
commits: rootCommitsBeforeChangelog,
|
|
4300
|
-
extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
|
|
4301
|
-
});
|
|
4302
|
-
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4303
|
-
subject: `release: ${level} bump`,
|
|
4304
|
-
version: rootVersion,
|
|
4305
|
-
tagName: rootVersion,
|
|
4306
|
-
commits: rootCommitsBeforeChangelog,
|
|
4307
|
-
changelog
|
|
4308
|
-
}));
|
|
4309
|
-
pushBranch(gitRoot, STAGING_BRANCH);
|
|
4310
|
-
const stagingCommit = headCommit(gitRoot);
|
|
4311
|
-
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
4312
|
-
const released = mergeBranchIntoTarget(root, {
|
|
4313
|
-
sourceBranch: STAGING_BRANCH,
|
|
4314
|
-
targetBranch: PRODUCTION_BRANCH,
|
|
4315
|
-
message: releaseAdminMessage({
|
|
4316
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4317
|
-
version: rootVersion,
|
|
4318
|
-
tagName: rootVersion,
|
|
4319
|
-
commits: rootCommits,
|
|
4320
|
-
changelog
|
|
4321
|
-
}),
|
|
4322
|
-
pushTarget: true
|
|
4323
|
-
});
|
|
4324
|
-
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
|
|
4325
|
-
subject: `release: ${rootVersion}`,
|
|
4326
|
-
version: rootVersion,
|
|
4327
|
-
tagName: rootVersion,
|
|
4328
|
-
commits: rootCommits,
|
|
4329
|
-
changelog
|
|
4330
|
-
}));
|
|
4331
|
-
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
4332
|
-
return {
|
|
4333
|
-
rootVersion,
|
|
4334
|
-
stagingCommit,
|
|
4335
|
-
releasedCommit: released.commitSha,
|
|
4336
|
-
tag,
|
|
4337
|
-
changelog,
|
|
4338
|
-
adminCommitSummary: {
|
|
4339
|
-
commitCount: rootCommits.length,
|
|
4340
|
-
notableCommits: rootCommits.slice(0, 12)
|
|
4341
|
-
}
|
|
4342
|
-
};
|
|
4343
|
-
});
|
|
4344
|
-
rootRepo.committed = true;
|
|
4345
|
-
rootRepo.pushed = true;
|
|
4346
|
-
rootRepo.merged = true;
|
|
4347
|
-
rootRepo.branch = PRODUCTION_BRANCH;
|
|
4348
|
-
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
4349
|
-
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
4350
|
-
const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
4351
|
-
hostedDeployGate({
|
|
4352
|
-
name: rootRepo.name,
|
|
4353
|
-
repoPath: rootRepo.path,
|
|
4354
|
-
workflow: "deploy.yml",
|
|
4355
|
-
branch: rootVersion,
|
|
4356
|
-
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
4357
|
-
})
|
|
4358
|
-
].filter((gate) => gate.headSha), ciMode, {
|
|
4359
|
-
root,
|
|
4360
|
-
runId: workflowRun.runId,
|
|
4361
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4362
|
-
}).then((workflowGates) => ({ workflowGates })));
|
|
4363
|
-
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
4364
|
-
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4365
|
-
const applicationSelection2 = selectWorkflowApplications(root, { changedPaths: ["treeseed.site.yaml"] });
|
|
4366
|
-
const hostingAudit2 = await runWorkflowHostedResourceVerification("release", root, helpers, "prod", {
|
|
4367
|
-
enabled: true,
|
|
4368
|
-
strict: effectiveInput.verifyDeployedResources === true,
|
|
4369
|
-
live: effectiveInput.verifyDeployedResources === true,
|
|
4370
|
-
appId: singleSelectedWorkflowAppId(applicationSelection2)
|
|
4371
|
-
});
|
|
4372
|
-
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
|
|
4373
|
-
version: rootVersion,
|
|
4374
|
-
changelog: rootRelease2?.changelog ?? null
|
|
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" }
|
|
4375
5171
|
}));
|
|
4376
|
-
const
|
|
4377
|
-
const
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
freshArchivedRuns: freshPreparation.archived,
|
|
4383
|
-
resumed: workflowRun.resumed,
|
|
4384
|
-
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
4385
|
-
autoResumed: autoResumeRun != null,
|
|
4386
|
-
rootVersion: String(rootRelease2?.rootVersion ?? ""),
|
|
4387
|
-
releaseTag: String(rootRelease2?.rootVersion ?? ""),
|
|
4388
|
-
releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
|
|
4389
|
-
stagingBranch: STAGING_BRANCH,
|
|
4390
|
-
productionBranch: PRODUCTION_BRANCH,
|
|
4391
|
-
touchedPackages: [],
|
|
4392
|
-
packageSelection: { changed: [], dependents: [], selected: [] },
|
|
4393
|
-
applicationSelection: applicationSelection2,
|
|
4394
|
-
publishWait: [],
|
|
4395
|
-
repos: [],
|
|
4396
|
-
rootRepo,
|
|
4397
|
-
releaseCandidate,
|
|
4398
|
-
stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
|
|
4399
|
-
releaseBackMerge: releaseBackMerge2,
|
|
4400
|
-
hostedDeploymentState: hostedDeploymentState2,
|
|
4401
|
-
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
4402
|
-
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
4403
|
-
workspaceLinks: workspaceLinks2,
|
|
4404
|
-
ciMode,
|
|
4405
|
-
workflowGates: [
|
|
4406
|
-
...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
|
|
4407
|
-
...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
|
|
4408
|
-
],
|
|
4409
|
-
hostingAudit: hostingAudit2,
|
|
4410
|
-
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
4411
|
-
};
|
|
4412
|
-
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
4413
|
-
return buildWorkflowResult("release", root, payload2, {
|
|
4414
|
-
runId: workflowRun.runId,
|
|
4415
|
-
nextSteps: createNextSteps([
|
|
4416
|
-
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
4417
|
-
])
|
|
4418
|
-
});
|
|
4419
|
-
}
|
|
4420
|
-
if (!resumeAtRootGates) {
|
|
4421
|
-
validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
|
|
4422
|
-
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
4423
|
-
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4424
|
-
prepareReleaseBranches(pkg.dir);
|
|
4425
|
-
}
|
|
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}`);
|
|
4426
5178
|
}
|
|
4427
|
-
}
|
|
4428
|
-
releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
4429
|
-
const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
4430
|
-
const releasedPackageDevTags2 = Object.fromEntries(
|
|
4431
|
-
checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
|
|
4432
|
-
const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
|
|
4433
|
-
return [pkg.name, String(packageJson.version ?? "")];
|
|
4434
|
-
}).filter(([, version]) => version.includes("-dev."))
|
|
4435
|
-
);
|
|
4436
|
-
const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveDependencyReplacementVersions);
|
|
4437
|
-
applyStableWorkspaceVersionChanges(root, effectiveVersions);
|
|
4438
|
-
setRootPackageJsonVersion(root, rootVersion);
|
|
4439
|
-
const releaseInstalls2 = [
|
|
4440
|
-
{ name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
|
|
4441
|
-
];
|
|
4442
|
-
assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
|
|
4443
5179
|
return {
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
5180
|
+
versions: Object.fromEntries(allVersions.entries()),
|
|
5181
|
+
adapterMetadata,
|
|
5182
|
+
rootInstall,
|
|
5183
|
+
workspaceUnlink
|
|
4447
5184
|
};
|
|
4448
|
-
}
|
|
4449
|
-
const
|
|
4450
|
-
const
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const report = findReportByName(packageReports, pkg.name);
|
|
4455
|
-
if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4456
|
-
if (report) {
|
|
4457
|
-
report.skippedReason = "unchanged";
|
|
4458
|
-
}
|
|
4459
|
-
continue;
|
|
4460
|
-
}
|
|
4461
|
-
const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
|
|
4462
|
-
checkoutBranch(pkg.dir, STAGING_BRANCH);
|
|
4463
|
-
const tagName = String(effectiveVersions.get(pkg.name));
|
|
4464
|
-
releaseInstalls.push({
|
|
4465
|
-
name: pkg.name,
|
|
4466
|
-
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
4467
|
-
});
|
|
4468
|
-
assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
|
|
4469
|
-
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}`, () => {
|
|
4470
5191
|
const changelog = updateReleaseChangelog(pkg.dir, {
|
|
4471
|
-
version
|
|
4472
|
-
|
|
4473
|
-
|
|
5192
|
+
version,
|
|
5193
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
5194
|
+
targetRef: "HEAD"
|
|
4474
5195
|
});
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
changelog,
|
|
4483
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4484
|
-
})], { cwd: pkg.dir });
|
|
4485
|
-
}
|
|
4486
|
-
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
4487
|
-
const packageCommits = releaseHistoryCommits(pkg.dir);
|
|
4488
|
-
const mergeResult = mergeBranchIntoTarget(pkg.dir, {
|
|
4489
|
-
sourceBranch: STAGING_BRANCH,
|
|
4490
|
-
targetBranch: PRODUCTION_BRANCH,
|
|
4491
|
-
message: releaseAdminMessage({
|
|
4492
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4493
|
-
version: tagName,
|
|
4494
|
-
tagName,
|
|
4495
|
-
commits: packageCommits,
|
|
4496
|
-
changelog,
|
|
4497
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4498
|
-
}),
|
|
4499
|
-
pushTarget: true,
|
|
4500
|
-
allowUnrelatedHistories: true
|
|
4501
|
-
});
|
|
4502
|
-
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
|
|
4503
|
-
subject: `release: ${tagName}`,
|
|
4504
|
-
version: tagName,
|
|
4505
|
-
tagName,
|
|
4506
|
-
commits: packageCommits,
|
|
4507
|
-
changelog,
|
|
4508
|
-
extraLines: [`Package: ${pkg.name}`]
|
|
4509
|
-
}));
|
|
4510
|
-
const workflowGates = await waitForWorkflowGates("release", [
|
|
4511
|
-
{
|
|
4512
|
-
name: pkg.name,
|
|
4513
|
-
repoPath: pkg.dir,
|
|
4514
|
-
workflow: "publish.yml",
|
|
4515
|
-
headSha: mergeResult.commitSha,
|
|
4516
|
-
branch: tagName
|
|
4517
|
-
}
|
|
4518
|
-
], ciMode, {
|
|
4519
|
-
root,
|
|
4520
|
-
runId: workflowRun.runId,
|
|
4521
|
-
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4522
|
-
});
|
|
4523
|
-
const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
|
|
4524
|
-
assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
|
|
4525
|
-
const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
|
|
4526
|
-
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
4527
|
-
version: tagName,
|
|
4528
|
-
tagName,
|
|
4529
|
-
sourceRef: PRODUCTION_BRANCH,
|
|
4530
|
-
targetRef: STAGING_BRANCH,
|
|
4531
|
-
commits: packageCommits,
|
|
4532
|
-
changelog,
|
|
4533
|
-
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
|
|
4534
5203
|
}));
|
|
4535
|
-
|
|
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}`);
|
|
4536
5207
|
return {
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
5208
|
+
name: pkg.name,
|
|
5209
|
+
path: relative(root, pkg.dir),
|
|
5210
|
+
version,
|
|
4540
5211
|
changelog,
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
},
|
|
4545
|
-
publish,
|
|
4546
|
-
workflowGates,
|
|
4547
|
-
backMerge
|
|
5212
|
+
commit,
|
|
5213
|
+
promotion,
|
|
5214
|
+
tag
|
|
4548
5215
|
};
|
|
4549
5216
|
});
|
|
4550
|
-
|
|
4551
|
-
report.pushed = true;
|
|
4552
|
-
report.merged = true;
|
|
4553
|
-
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
4554
|
-
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
4555
|
-
report.publishWait = releasedPackage?.publish ?? null;
|
|
4556
|
-
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
4557
|
-
report.backMerge = releasedPackage?.backMerge ?? null;
|
|
4558
|
-
report.changelog = releasedPackage?.changelog ?? null;
|
|
4559
|
-
report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
|
|
4560
|
-
report.branch = STAGING_BRANCH;
|
|
4561
|
-
publishWait.push({
|
|
4562
|
-
name: report.name,
|
|
4563
|
-
...releasedPackage?.publish ?? {}
|
|
4564
|
-
});
|
|
5217
|
+
packageReleases.push(packageRelease);
|
|
4565
5218
|
}
|
|
4566
|
-
assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
|
|
4567
5219
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
commits: rootCommitsBeforeChangelog,
|
|
4574
|
-
extraDependencyBullets: [
|
|
4575
|
-
`Release @treeseed/market ${rootVersion}.`,
|
|
4576
|
-
...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
|
|
4577
|
-
]
|
|
4578
|
-
});
|
|
4579
|
-
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4580
|
-
subject: `release: ${level} bump`,
|
|
4581
|
-
version: rootVersion,
|
|
4582
|
-
tagName: rootVersion,
|
|
4583
|
-
commits: rootCommitsBeforeChangelog,
|
|
4584
|
-
changelog,
|
|
4585
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
4586
|
-
}));
|
|
4587
|
-
pushBranch(gitRoot, STAGING_BRANCH);
|
|
4588
|
-
const stagingCommit = headCommit(gitRoot);
|
|
4589
|
-
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
4590
|
-
let released;
|
|
4591
|
-
let submoduleReconciliation = null;
|
|
4592
|
-
const mergeMessage = releaseAdminMessage({
|
|
4593
|
-
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
4594
|
-
version: rootVersion,
|
|
4595
|
-
tagName: rootVersion,
|
|
4596
|
-
commits: rootCommits,
|
|
4597
|
-
changelog,
|
|
4598
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
5220
|
+
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
5221
|
+
version: plannedRelease.rootVersion,
|
|
5222
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
5223
|
+
targetRef: "HEAD",
|
|
5224
|
+
extraDependencyBullets: versionLines(stableVersions)
|
|
4599
5225
|
});
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
pushTarget: false,
|
|
4606
|
-
quietMerge: true
|
|
4607
|
-
});
|
|
4608
|
-
} catch (error) {
|
|
4609
|
-
const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
|
|
4610
|
-
if (!reconciliation.resolved) {
|
|
4611
|
-
throw error;
|
|
4612
|
-
}
|
|
4613
|
-
helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
|
|
4614
|
-
submoduleReconciliation = reconciliation;
|
|
4615
|
-
commitAllIfChanged(gitRoot, mergeMessage);
|
|
4616
|
-
released = { commitSha: headCommit(gitRoot) };
|
|
4617
|
-
}
|
|
4618
|
-
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
4619
|
-
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
4620
|
-
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
|
|
4624
|
-
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4625
|
-
subject: "release: sync package main heads",
|
|
4626
|
-
version: rootVersion,
|
|
4627
|
-
tagName: rootVersion,
|
|
4628
|
-
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,
|
|
4629
5231
|
targetRef: PRODUCTION_BRANCH,
|
|
4630
|
-
commits: mainPointerCommits,
|
|
4631
|
-
changelog,
|
|
4632
|
-
extraLines: versionLines(effectiveVersions).map((line) => `Main package ${line}`)
|
|
4633
|
-
}));
|
|
4634
|
-
const releasedCommit = headCommit(gitRoot);
|
|
4635
|
-
const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit, releaseAdminMessage({
|
|
4636
|
-
subject: `release: ${rootVersion}`,
|
|
4637
|
-
version: rootVersion,
|
|
4638
|
-
tagName: rootVersion,
|
|
4639
|
-
commits: rootCommits,
|
|
4640
5232
|
changelog,
|
|
4641
|
-
extraLines: versionLines(
|
|
5233
|
+
extraLines: versionLines(stableVersions).map((line) => `Released package ${line}`)
|
|
4642
5234
|
}));
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
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}`);
|
|
4646
5238
|
return {
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
mergeCommit: released.commitSha,
|
|
4651
|
-
tag,
|
|
5239
|
+
name: "@treeseed/market",
|
|
5240
|
+
version: plannedRelease.rootVersion,
|
|
5241
|
+
releaseTag: plannedRelease.releaseTag,
|
|
4652
5242
|
changelog,
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
},
|
|
4657
|
-
submoduleReconciliation
|
|
5243
|
+
commit,
|
|
5244
|
+
promotion,
|
|
5245
|
+
tag
|
|
4658
5246
|
};
|
|
4659
5247
|
});
|
|
4660
|
-
|
|
4661
|
-
rootRepo.pushed = true;
|
|
4662
|
-
rootRepo.merged = true;
|
|
4663
|
-
rootRepo.branch = PRODUCTION_BRANCH;
|
|
4664
|
-
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
4665
|
-
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
4666
|
-
const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
5248
|
+
const publishGates = [
|
|
4667
5249
|
hostedDeployGate({
|
|
4668
|
-
name:
|
|
4669
|
-
repoPath:
|
|
4670
|
-
workflow: "deploy.yml",
|
|
4671
|
-
branch:
|
|
4672
|
-
headSha: String(rootRelease
|
|
4673
|
-
|
|
4674
|
-
|
|
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, {
|
|
4675
5270
|
root,
|
|
4676
5271
|
runId: workflowRun.runId,
|
|
4677
5272
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4678
5273
|
}).then((workflowGates) => ({ workflowGates })));
|
|
4679
|
-
const
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
selectedVersions: effectiveVersions
|
|
4692
|
-
}));
|
|
4693
|
-
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
4694
|
-
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", () => {
|
|
4695
|
-
const cleanup = collectStaleDevTagCleanupReports(root, {
|
|
4696
|
-
includePackages: effectivePackageSelection.selected,
|
|
4697
|
-
branchScope: "all"
|
|
4698
|
-
}, { execute: true });
|
|
4699
|
-
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 };
|
|
4700
5286
|
});
|
|
4701
|
-
|
|
4702
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
5287
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
4703
5288
|
const payload = {
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
stagingBranch: STAGING_BRANCH,
|
|
4716
|
-
productionBranch: PRODUCTION_BRANCH,
|
|
4717
|
-
touchedPackages: effectivePackageSelection.selected,
|
|
4718
|
-
packageSelection: effectivePackageSelection,
|
|
4719
|
-
applicationSelection,
|
|
4720
|
-
replacedDevReferences,
|
|
4721
|
-
releaseInstalls,
|
|
4722
|
-
devTagCleanup,
|
|
4723
|
-
publishWait,
|
|
4724
|
-
repos: packageReports,
|
|
4725
|
-
rootRepo,
|
|
4726
|
-
releaseCandidate,
|
|
4727
|
-
stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
|
|
4728
|
-
releaseBackMerge,
|
|
4729
|
-
hostedDeploymentState,
|
|
4730
|
-
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
4731
|
-
pushStatus: {
|
|
4732
|
-
stagingPushed: true,
|
|
4733
|
-
productionPushed: true,
|
|
4734
|
-
tagPushed: true
|
|
4735
|
-
},
|
|
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,
|
|
4736
5300
|
workspaceLinks,
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
...packageReports.flatMap((report) => report.workflowGates),
|
|
4741
|
-
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
4742
|
-
],
|
|
4743
|
-
hostingAudit,
|
|
4744
|
-
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
5301
|
+
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|
|
5302
|
+
touchedPackages: selectedPackageNames,
|
|
5303
|
+
finalBranch: STAGING_BRANCH
|
|
4745
5304
|
};
|
|
4746
5305
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
4747
5306
|
return buildWorkflowResult("release", root, payload, {
|
|
4748
5307
|
runId: workflowRun.runId,
|
|
5308
|
+
summary: "Treeseed production release completed successfully.",
|
|
4749
5309
|
nextSteps: createNextSteps([
|
|
4750
|
-
{ operation: "status", reason: "Inspect release
|
|
5310
|
+
{ operation: "status", reason: "Inspect release state after production promotion." }
|
|
4751
5311
|
])
|
|
4752
5312
|
});
|
|
4753
5313
|
} catch (error) {
|
|
4754
|
-
const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
|
|
4755
|
-
const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
|
|
4756
|
-
const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
|
|
4757
|
-
const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
|
|
4758
|
-
helpers.write(`[release][recovery] Last release phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`, "stderr");
|
|
4759
5314
|
try {
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
});
|
|
4763
|
-
if (repair.blockers.length > 0) {
|
|
4764
|
-
helpers.write(`[release][recovery] Package repos need manual review before retrying:
|
|
4765
|
-
${repair.blockers.join("\n")}`, "stderr");
|
|
4766
|
-
}
|
|
4767
|
-
} catch (repairError) {
|
|
4768
|
-
helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
|
|
4769
|
-
}
|
|
4770
|
-
if (localCleanup.restored.length > 0) {
|
|
4771
|
-
helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
|
|
4772
|
-
}
|
|
4773
|
-
if (localCleanup.manualReview.length > 0) {
|
|
4774
|
-
helpers.write(`[release][recovery] Local cleanup needs manual review:
|
|
4775
|
-
${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 {
|
|
4776
5317
|
}
|
|
4777
|
-
helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
|
|
4778
5318
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
4779
5319
|
resumable: true,
|
|
4780
5320
|
runId: workflowRun.runId,
|
|
4781
5321
|
command: "release",
|
|
4782
|
-
message:
|
|
4783
|
-
recoverCommand: "
|
|
4784
|
-
resumeCommand: `
|
|
4785
|
-
localCleanup
|
|
5322
|
+
message: "Resume the interrupted production release after fixing the cause.",
|
|
5323
|
+
recoverCommand: "treeseed recover",
|
|
5324
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
4786
5325
|
});
|
|
4787
5326
|
throw error;
|
|
4788
5327
|
}
|
|
@@ -4844,6 +5383,8 @@ async function workflowResume(helpers, input) {
|
|
|
4844
5383
|
return workflowSwitch(resumedHelpers, journal.input);
|
|
4845
5384
|
case "save":
|
|
4846
5385
|
return workflowSave(resumedHelpers, journal.input);
|
|
5386
|
+
case "update":
|
|
5387
|
+
return workflowUpdate(resumedHelpers, journal.input);
|
|
4847
5388
|
case "close":
|
|
4848
5389
|
return workflowClose(resumedHelpers, journal.input);
|
|
4849
5390
|
case "stage":
|
|
@@ -4866,7 +5407,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
4866
5407
|
try {
|
|
4867
5408
|
return await withContextEnv(helpers.context.env, async () => {
|
|
4868
5409
|
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
4869
|
-
const lock = inspectWorkflowLock(root);
|
|
5410
|
+
const lock = inspectWorkflowLock(root, { scope: "worktree" });
|
|
4870
5411
|
const journals = listWorkflowRunJournals(root);
|
|
4871
5412
|
const session = resolveTreeseedWorkflowSession(root);
|
|
4872
5413
|
const currentHeads = Object.fromEntries(
|
|
@@ -5107,19 +5648,27 @@ async function workflowDestroy(helpers, input) {
|
|
|
5107
5648
|
}
|
|
5108
5649
|
export {
|
|
5109
5650
|
TreeseedWorkflowError,
|
|
5651
|
+
destroyTreeseedBranchPreview,
|
|
5652
|
+
normalizeReleaseCandidateMode,
|
|
5653
|
+
normalizeSaveCiMode,
|
|
5654
|
+
normalizeSaveLane,
|
|
5655
|
+
reconcileTreeseedBranchPreview,
|
|
5656
|
+
shouldUseHostedSaveCi,
|
|
5110
5657
|
workflowCi,
|
|
5111
5658
|
workflowClose,
|
|
5112
5659
|
workflowConfig,
|
|
5113
5660
|
workflowDestroy,
|
|
5114
5661
|
workflowDev,
|
|
5115
5662
|
workflowExport,
|
|
5663
|
+
workflowProof,
|
|
5116
5664
|
workflowRecover,
|
|
5117
5665
|
workflowRelease,
|
|
5666
|
+
workflowReleaseCandidate,
|
|
5118
5667
|
workflowResume,
|
|
5119
5668
|
workflowSave,
|
|
5120
5669
|
workflowStage,
|
|
5121
5670
|
workflowStatus,
|
|
5122
5671
|
workflowSwitch,
|
|
5123
|
-
|
|
5124
|
-
|
|
5672
|
+
workflowTasks,
|
|
5673
|
+
workflowUpdate
|
|
5125
5674
|
};
|