@treeseed/sdk 0.12.43 → 0.12.45
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/dist/guarantees/index.d.ts +83 -0
- package/dist/guarantees/index.js +688 -86
- package/dist/hosting/graph.js +40 -6
- package/dist/operations/services/git-workflow.d.ts +16 -1
- package/dist/operations/services/git-workflow.js +65 -5
- package/dist/operations/services/github-api.js +0 -19
- package/dist/operations/services/hosted-service-checks.js +17 -1
- package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
- package/dist/operations/services/live-hosted-service-checks.js +7 -4
- package/dist/operations/services/local-cleanup.js +3 -7
- package/dist/operations/services/package-adapters.d.ts +14 -0
- package/dist/operations/services/package-adapters.js +36 -3
- package/dist/operations/services/package-artifacts.d.ts +37 -0
- package/dist/operations/services/package-artifacts.js +99 -0
- package/dist/operations/services/railway-deploy.js +78 -18
- package/dist/operations/services/railway-source-policy.d.ts +19 -0
- package/dist/operations/services/railway-source-policy.js +66 -0
- package/dist/operations/services/repository-save-orchestrator.js +12 -5
- package/dist/platform/desired-state.js +2 -3
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/providers/railway-iac.d.ts +2 -0
- package/dist/reconcile/providers/railway-iac.js +31 -3
- package/dist/scenes/builtin-plugins.js +36 -5
- package/dist/scenes/device-matrix.js +2 -0
- package/dist/scenes/environment.js +1 -1
- package/dist/scenes/runner.js +28 -16
- package/dist/scenes/schema.js +31 -2
- package/dist/scenes/types.d.ts +25 -2
- package/dist/scenes/visual-audit-fixtures.js +9 -3
- package/dist/workflow/operations.d.ts +27 -92
- package/dist/workflow/operations.js +236 -144
- package/dist/workflow/runs.d.ts +1 -0
- package/dist/workflow/runs.js +57 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +4 -1
|
@@ -1,6 +1,7 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
4
5
|
import {
|
|
5
6
|
compileTreeseedDesiredResourceGraph,
|
|
6
7
|
compileTreeseedDesiredUnitsFromGraph
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
assertFeatureBranch,
|
|
56
57
|
branchExists,
|
|
57
58
|
checkoutBranch,
|
|
59
|
+
checkoutNewTaskBranchWithChanges,
|
|
58
60
|
checkoutTaskBranchFromStaging,
|
|
59
61
|
createDeprecatedTaskTag,
|
|
60
62
|
deleteLocalBranch,
|
|
@@ -71,8 +73,7 @@ import {
|
|
|
71
73
|
remoteHeadCommit,
|
|
72
74
|
remoteBranchExists,
|
|
73
75
|
STAGING_BRANCH,
|
|
74
|
-
syncBranchWithOrigin
|
|
75
|
-
waitForStagingAutomation
|
|
76
|
+
syncBranchWithOrigin
|
|
76
77
|
} from "../operations/services/git-workflow.js";
|
|
77
78
|
import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
|
|
78
79
|
import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
|
|
@@ -340,9 +341,10 @@ function normalizeSceneArtifactsMode(value) {
|
|
|
340
341
|
return value === "screenshots" ? "screenshots" : "full";
|
|
341
342
|
}
|
|
342
343
|
function maybeRunLocalWorkflowCleanup(helpers, root, operation, input) {
|
|
344
|
+
if (operation !== "release") return null;
|
|
343
345
|
if (normalizeExecutionMode(input) === "plan" || input.skipCleanup === true) return null;
|
|
344
|
-
helpers.write(
|
|
345
|
-
return runTreeseedLocalCleanup({ root, mode: "
|
|
346
|
+
helpers.write("Treeseed release cleanup: pruning disposable local build state while preserving package caches and release evidence.", "stderr");
|
|
347
|
+
return runTreeseedLocalCleanup({ root, mode: "standard", docker: false, npmCache: false });
|
|
346
348
|
}
|
|
347
349
|
function normalizeSaveCiMode(mode, branch, lane = "fast") {
|
|
348
350
|
if (mode === "hosted" || mode === "off") return mode;
|
|
@@ -372,11 +374,13 @@ function normalizeReleaseCandidateMode(mode, operation, lane = "fast") {
|
|
|
372
374
|
if (value === "hybrid" || value === "strict" || value === "skip") {
|
|
373
375
|
return value;
|
|
374
376
|
}
|
|
375
|
-
|
|
376
|
-
return operation === "save" ? "hybrid" : "strict";
|
|
377
|
+
return operation === "save" ? "skip" : "strict";
|
|
377
378
|
}
|
|
378
379
|
function shouldUseHostedSaveCi(input, branch, lane = normalizeSaveLane(input.lane)) {
|
|
379
|
-
|
|
380
|
+
void input;
|
|
381
|
+
void branch;
|
|
382
|
+
void lane;
|
|
383
|
+
return false;
|
|
380
384
|
}
|
|
381
385
|
function worktreePayload(root, requestedMode) {
|
|
382
386
|
const metadata = managedWorkflowWorktreeMetadata(root);
|
|
@@ -643,6 +647,9 @@ ${liveFailures.join("\n")}`, {
|
|
|
643
647
|
};
|
|
644
648
|
}
|
|
645
649
|
async function runReleaseWebLiveVerification(root, environment, helpers, operation) {
|
|
650
|
+
if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
|
|
651
|
+
return { status: "skipped", environment, reason: "release gates disabled" };
|
|
652
|
+
}
|
|
646
653
|
const env = {
|
|
647
654
|
...helpers.context.env,
|
|
648
655
|
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
|
|
@@ -705,65 +712,35 @@ function productionReleaseImageRefEnv(selectedVersions) {
|
|
|
705
712
|
function productionReleaseImageRefVersions(root, selectedVersions) {
|
|
706
713
|
const versions = new Map(selectedVersions);
|
|
707
714
|
for (const adapter of discoverTreeseedPackageAdapters(root)) {
|
|
708
|
-
if (adapter.id !== "treedx" && adapter.id !== "@treeseed/treedx") continue;
|
|
709
715
|
const prodSource = stringRecord(adapter.metadata.deploymentSource)?.prod;
|
|
710
|
-
|
|
716
|
+
const imageBackedPackage = ["@treeseed/api", "@treeseed/agent", "treedx", "@treeseed/treedx"].includes(adapter.id);
|
|
717
|
+
if (prodSource !== "image" && !imageBackedPackage) continue;
|
|
711
718
|
if (versions.has(adapter.id) || !adapter.version) continue;
|
|
712
719
|
const line = stableVersionLine(adapter.version);
|
|
713
720
|
const stableVersion = (line ? highestStableGitTagOnLine(adapter.dir, line) : null) ?? adapter.version;
|
|
714
721
|
versions.set(adapter.id, stableVersion);
|
|
715
722
|
}
|
|
723
|
+
for (const [packageName, relativePath] of [["@treeseed/api", "packages/api"], ["@treeseed/agent", "packages/agent"]]) {
|
|
724
|
+
if (versions.has(packageName)) continue;
|
|
725
|
+
const packageRoot = resolve(root, relativePath);
|
|
726
|
+
const packageJsonPath = resolve(packageRoot, "package.json");
|
|
727
|
+
if (!existsSync(packageJsonPath)) continue;
|
|
728
|
+
const version = stringRecord(JSON.parse(readFileSync(packageJsonPath, "utf8"))).version;
|
|
729
|
+
if (typeof version !== "string") continue;
|
|
730
|
+
const line = stableVersionLine(version);
|
|
731
|
+
versions.set(packageName, (line ? highestStableGitTagOnLine(packageRoot, line) : null) ?? version);
|
|
732
|
+
}
|
|
716
733
|
return versions;
|
|
717
734
|
}
|
|
718
735
|
function stableVersionLine(version) {
|
|
719
|
-
const match = version.match(/^(\d+\.\d+)\.\d
|
|
736
|
+
const match = version.match(/^(\d+\.\d+)\.\d+(?:-[0-9A-Za-z.-]+)?$/u);
|
|
720
737
|
return match?.[1] ?? null;
|
|
721
738
|
}
|
|
722
|
-
async function runReleaseApiGuarantees(root, environment, helpers, operation, sceneArtifacts) {
|
|
723
|
-
const env = {
|
|
724
|
-
...helpers.context.env,
|
|
725
|
-
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
|
|
726
|
-
};
|
|
727
|
-
env.TREESEED_ACCEPTANCE_SERVICE_ID ??= env.TREESEED_API_WEB_SERVICE_ID ?? env.TREESEED_WEB_SERVICE_ID;
|
|
728
|
-
env.TREESEED_ACCEPTANCE_SERVICE_SECRET ??= env.TREESEED_API_WEB_SERVICE_SECRET ?? env.TREESEED_WEB_SERVICE_SECRET;
|
|
729
|
-
if (!env.TREESEED_ACCEPTANCE_SERVICE_ID || !env.TREESEED_ACCEPTANCE_SERVICE_SECRET) {
|
|
730
|
-
workflowError(operation, "release_gate_failed", `${environment} API release guarantees cannot run because API acceptance service credentials are missing.`, {
|
|
731
|
-
details: {
|
|
732
|
-
environment,
|
|
733
|
-
missing: [
|
|
734
|
-
!env.TREESEED_ACCEPTANCE_SERVICE_ID ? "TREESEED_ACCEPTANCE_SERVICE_ID" : null,
|
|
735
|
-
!env.TREESEED_ACCEPTANCE_SERVICE_SECRET ? "TREESEED_ACCEPTANCE_SERVICE_SECRET" : null
|
|
736
|
-
].filter((value) => Boolean(value))
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
helpers.write(`[${operation}][workflow] Running ${environment} API release guarantees before root deployment.`);
|
|
741
|
-
return await withContextEnv(env, async () => {
|
|
742
|
-
const report = await runTreeseedGuarantees({
|
|
743
|
-
workspaceRoot: root,
|
|
744
|
-
filter: { ownerPackage: "@treeseed/api" },
|
|
745
|
-
environment,
|
|
746
|
-
evidenceTarget: "release",
|
|
747
|
-
sceneArtifacts
|
|
748
|
-
});
|
|
749
|
-
if (!report.ok) {
|
|
750
|
-
const diagnostics = report.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20).map((entry) => `${entry.code}: ${entry.message}${entry.sourcePath ? ` (${entry.sourcePath})` : ""}`);
|
|
751
|
-
workflowError(operation, "release_gate_failed", `API release guarantees for ${environment} failed:
|
|
752
|
-
${diagnostics.join("\n") || `See ${report.outputRoot}`}`, {
|
|
753
|
-
details: { environment, outputRoot: report.outputRoot, counts: report.counts, diagnostics: report.diagnostics }
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
return {
|
|
757
|
-
ok: report.ok,
|
|
758
|
-
environment: report.environment,
|
|
759
|
-
runId: report.runId,
|
|
760
|
-
outputRoot: report.outputRoot,
|
|
761
|
-
counts: report.counts
|
|
762
|
-
};
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
739
|
async function runReleaseProductionGuarantees(root, helpers, operation, sceneArtifacts) {
|
|
766
740
|
const environment = "prod";
|
|
741
|
+
if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
|
|
742
|
+
return { ok: true, status: "skipped", environment, reason: "release gates disabled" };
|
|
743
|
+
}
|
|
767
744
|
const env = {
|
|
768
745
|
...helpers.context.env,
|
|
769
746
|
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
|
|
@@ -785,6 +762,7 @@ async function runReleaseProductionGuarantees(root, helpers, operation, sceneArt
|
|
|
785
762
|
return await withContextEnv(env, async () => {
|
|
786
763
|
const report = await runTreeseedGuarantees({
|
|
787
764
|
workspaceRoot: root,
|
|
765
|
+
filter: { gate: "smoke", status: "active" },
|
|
788
766
|
environment,
|
|
789
767
|
evidenceTarget: "release",
|
|
790
768
|
sceneArtifacts
|
|
@@ -819,7 +797,7 @@ function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGa
|
|
|
819
797
|
{ scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
|
|
820
798
|
{ scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
|
|
821
799
|
]) {
|
|
822
|
-
const gate = gates.find((candidate) => candidate.workflow === "deploy
|
|
800
|
+
const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
|
|
823
801
|
const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
|
|
824
802
|
if (!gate || !timestamp) {
|
|
825
803
|
continue;
|
|
@@ -1360,7 +1338,7 @@ function defaultCiWorkflows(kind, branch) {
|
|
|
1360
1338
|
return ["verify.yml"];
|
|
1361
1339
|
}
|
|
1362
1340
|
if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
|
|
1363
|
-
return ["deploy
|
|
1341
|
+
return ["deploy.yml"];
|
|
1364
1342
|
}
|
|
1365
1343
|
return ["verify.yml"];
|
|
1366
1344
|
}
|
|
@@ -1710,34 +1688,50 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
1710
1688
|
function workflowFileExists(repoPath, workflow) {
|
|
1711
1689
|
return existsSync(resolve(repoPath, ".github", "workflows", workflow));
|
|
1712
1690
|
}
|
|
1713
|
-
function
|
|
1714
|
-
const
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1691
|
+
function hostedWorkflowsForSavedRepository(root, repo, adapter) {
|
|
1692
|
+
const workflows = [];
|
|
1693
|
+
const addWorkflow = (workflow) => {
|
|
1694
|
+
if (!workflow) return;
|
|
1695
|
+
const normalized = workflow.trim().replace(/^\.github\/workflows\//u, "");
|
|
1696
|
+
if (normalized && !workflows.includes(normalized)) {
|
|
1697
|
+
workflows.push(normalized);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1717
1700
|
if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
|
|
1718
|
-
|
|
1701
|
+
addWorkflow("deploy.yml");
|
|
1702
|
+
} else {
|
|
1703
|
+
const fallbackAdapter = adapter ?? new Map(discoverTreeseedPackageAdapters(root).map((entry) => [resolve(entry.dir), entry])).get(resolve(repo.path));
|
|
1704
|
+
const adapterWorkflow = packageHostedVerifyWorkflow(fallbackAdapter);
|
|
1705
|
+
addWorkflow(adapterWorkflow);
|
|
1719
1706
|
}
|
|
1720
|
-
if (workflowFileExists(repo.path, "verify.yml"))
|
|
1721
|
-
return
|
|
1707
|
+
if (workflows.length === 0 && workflowFileExists(repo.path, "verify.yml")) addWorkflow("verify.yml");
|
|
1708
|
+
return workflows;
|
|
1722
1709
|
}
|
|
1723
1710
|
function gatesForSavedRepositoryReports(root, reports) {
|
|
1711
|
+
const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
|
|
1724
1712
|
return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch && (repo.committed || repo.tagName)).flatMap((repo) => {
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1713
|
+
const adapter = adapterByPath.get(resolve(repo.path));
|
|
1714
|
+
return hostedWorkflowsForSavedRepository(root, repo, adapter).map((workflow) => {
|
|
1715
|
+
const gate = {
|
|
1716
|
+
name: repo.name,
|
|
1717
|
+
repoPath: repo.path,
|
|
1718
|
+
workflow,
|
|
1719
|
+
branch: String(repo.branch),
|
|
1720
|
+
headSha: String(repo.commitSha),
|
|
1721
|
+
...packageHostedVerifyTimeoutSeconds(adapter) ? { timeoutSeconds: packageHostedVerifyTimeoutSeconds(adapter) } : {}
|
|
1722
|
+
};
|
|
1723
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
1724
|
+
});
|
|
1735
1725
|
});
|
|
1736
1726
|
}
|
|
1737
1727
|
function packageHostedVerifyWorkflow(adapter) {
|
|
1738
1728
|
const workflow = adapter?.metadata?.hostedVerifyWorkflow;
|
|
1739
1729
|
return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
|
|
1740
1730
|
}
|
|
1731
|
+
function packageHostedVerifyTimeoutSeconds(adapter) {
|
|
1732
|
+
const timeoutSeconds = adapter?.metadata?.hostedVerifyTimeoutSeconds;
|
|
1733
|
+
return typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) : null;
|
|
1734
|
+
}
|
|
1741
1735
|
function gateForSavedRootReport(report, branch, scope) {
|
|
1742
1736
|
if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
|
|
1743
1737
|
return [];
|
|
@@ -2148,7 +2142,7 @@ function validateStagingWorkflowContracts(root) {
|
|
|
2148
2142
|
return;
|
|
2149
2143
|
}
|
|
2150
2144
|
const missing = [];
|
|
2151
|
-
for (const fileName of ["verify.yml", "deploy
|
|
2145
|
+
for (const fileName of ["verify.yml", "deploy.yml"]) {
|
|
2152
2146
|
if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
|
|
2153
2147
|
missing.push(fileName);
|
|
2154
2148
|
}
|
|
@@ -2615,7 +2609,17 @@ async function workflowReleaseCandidate(helpers, input = {}) {
|
|
|
2615
2609
|
}
|
|
2616
2610
|
}
|
|
2617
2611
|
function buildReleasePlanSnapshot(input) {
|
|
2618
|
-
const
|
|
2612
|
+
const publishablePackageNames = new Set(
|
|
2613
|
+
discoverTreeseedPackageAdapters(input.root).filter((adapter) => adapter.capabilities.publish).map((adapter) => adapter.id)
|
|
2614
|
+
);
|
|
2615
|
+
const selectedPackageNames = new Set(
|
|
2616
|
+
input.packageSelection.selected.filter((name) => publishablePackageNames.has(name))
|
|
2617
|
+
);
|
|
2618
|
+
const publishablePackageSelection = {
|
|
2619
|
+
changed: input.packageSelection.changed.filter((name) => selectedPackageNames.has(name)),
|
|
2620
|
+
dependents: input.packageSelection.dependents.filter((name) => selectedPackageNames.has(name)),
|
|
2621
|
+
selected: [...selectedPackageNames]
|
|
2622
|
+
};
|
|
2619
2623
|
const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
|
|
2620
2624
|
const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
|
|
2621
2625
|
if (input.repairVersionLine !== true) {
|
|
@@ -2626,7 +2630,7 @@ function buildReleasePlanSnapshot(input) {
|
|
|
2626
2630
|
}
|
|
2627
2631
|
}
|
|
2628
2632
|
const plannedSelected = orderReleasePackageNames([...versionPlan.selected].filter((name) => versionPlan.versions.has(name)));
|
|
2629
|
-
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(
|
|
2633
|
+
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(publishablePackageSelection.changed.filter((name) => plannedSelected.includes(name))));
|
|
2630
2634
|
const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
|
|
2631
2635
|
const plannedPackageSelection = {
|
|
2632
2636
|
changed: plannedChanged,
|
|
@@ -3008,6 +3012,9 @@ async function collectPublishedReleaseArtifactChecks(selectedVersions) {
|
|
|
3008
3012
|
return checks;
|
|
3009
3013
|
}
|
|
3010
3014
|
async function verifyPublishedReleaseArtifacts(selectedVersions) {
|
|
3015
|
+
if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
|
|
3016
|
+
return { checks: [] };
|
|
3017
|
+
}
|
|
3011
3018
|
let checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
|
|
3012
3019
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
3013
3020
|
while (checks.some((check) => !check.ok) && Date.now() < deadline) {
|
|
@@ -3766,8 +3773,9 @@ async function workflowSwitch(helpers, input) {
|
|
|
3766
3773
|
});
|
|
3767
3774
|
const session = resolveTreeseedWorkflowSession(root);
|
|
3768
3775
|
const preview = input.preview === true;
|
|
3776
|
+
const adoptChanges = input.adoptChanges === true;
|
|
3769
3777
|
const executionMode = normalizeExecutionMode(input);
|
|
3770
|
-
if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
3778
|
+
if (executionMode !== "plan" && !adoptChanges && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
3771
3779
|
const managed = ensureManagedWorkflowWorktree({
|
|
3772
3780
|
root,
|
|
3773
3781
|
branchName,
|
|
@@ -3820,7 +3828,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3820
3828
|
previewRequested: preview,
|
|
3821
3829
|
worktreeMode: input.worktreeMode ?? "auto",
|
|
3822
3830
|
worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
|
|
3823
|
-
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
3831
|
+
blockers: !adoptChanges && dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
3824
3832
|
plannedSteps: [
|
|
3825
3833
|
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
3826
3834
|
...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
|
|
@@ -3837,7 +3845,17 @@ async function workflowSwitch(helpers, input) {
|
|
|
3837
3845
|
}
|
|
3838
3846
|
);
|
|
3839
3847
|
}
|
|
3840
|
-
if (
|
|
3848
|
+
if (adoptChanges) {
|
|
3849
|
+
const reports = [rootRepo, ...packageReports];
|
|
3850
|
+
const existingTargets = reports.filter((report) => branchExists(report.path, branchName) || remoteBranchExists(report.path, branchName));
|
|
3851
|
+
if (existingTargets.length > 0) {
|
|
3852
|
+
workflowError("switch", "validation_failed", `--adopt-changes requires a new branch; ${branchName} already exists in ${existingTargets.map((report) => report.name).join(", ")}.`);
|
|
3853
|
+
}
|
|
3854
|
+
const unsafeDirtyRepos = reports.filter((report) => report.dirty && currentBranch(report.path) !== STAGING_BRANCH);
|
|
3855
|
+
if (unsafeDirtyRepos.length > 0) {
|
|
3856
|
+
workflowError("switch", "validation_failed", `--adopt-changes only accepts dirty staging repositories: ${unsafeDirtyRepos.map((report) => report.name).join(", ")}.`);
|
|
3857
|
+
}
|
|
3858
|
+
} else if (mode === "recursive-workspace") {
|
|
3841
3859
|
assertWorkspaceClean(root);
|
|
3842
3860
|
assertSessionBranchSafety("switch", session);
|
|
3843
3861
|
} else {
|
|
@@ -3846,7 +3864,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3846
3864
|
const workflowRun = acquireWorkflowRun(
|
|
3847
3865
|
"switch",
|
|
3848
3866
|
session,
|
|
3849
|
-
{ branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
|
|
3867
|
+
{ branch: branchName, preview, adoptChanges, worktreeMode: input.worktreeMode ?? "auto" },
|
|
3850
3868
|
[
|
|
3851
3869
|
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
3852
3870
|
...packageReports.map((report) => ({
|
|
@@ -3867,7 +3885,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3867
3885
|
root,
|
|
3868
3886
|
workflowRun.runId,
|
|
3869
3887
|
"switch-root",
|
|
3870
|
-
() => checkoutTaskBranchFromStaging(repoDir, branchName, {
|
|
3888
|
+
() => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(repoDir, branchName, {
|
|
3871
3889
|
createIfMissing: input.createIfMissing !== false,
|
|
3872
3890
|
pushIfCreated: true
|
|
3873
3891
|
})
|
|
@@ -3886,7 +3904,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3886
3904
|
root,
|
|
3887
3905
|
workflowRun.runId,
|
|
3888
3906
|
`switch-${report.name}`,
|
|
3889
|
-
() => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
|
|
3907
|
+
() => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(managedRepo.dir, branchName, {
|
|
3890
3908
|
createIfMissing: input.createIfMissing !== false,
|
|
3891
3909
|
pushIfCreated: false
|
|
3892
3910
|
})
|
|
@@ -3925,7 +3943,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
3925
3943
|
workspaceLinks,
|
|
3926
3944
|
...worktreePayload(root, input.worktreeMode),
|
|
3927
3945
|
preconditions: {
|
|
3928
|
-
cleanWorktreeRequired:
|
|
3946
|
+
cleanWorktreeRequired: !adoptChanges,
|
|
3947
|
+
adoptedDirtyStagingChanges: adoptChanges,
|
|
3929
3948
|
baseBranch: STAGING_BRANCH
|
|
3930
3949
|
}
|
|
3931
3950
|
};
|
|
@@ -4427,8 +4446,8 @@ async function workflowSave(helpers, input) {
|
|
|
4427
4446
|
const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "save", effectiveInput);
|
|
4428
4447
|
const message = String(effectiveInput.message ?? "").trim();
|
|
4429
4448
|
const saveLane = normalizeSaveLane(effectiveInput.lane);
|
|
4430
|
-
const saveCiMode =
|
|
4431
|
-
const releaseCandidateMode =
|
|
4449
|
+
const saveCiMode = "off";
|
|
4450
|
+
const releaseCandidateMode = "skip";
|
|
4432
4451
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
4433
4452
|
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
4434
4453
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
@@ -4614,7 +4633,7 @@ async function workflowSave(helpers, input) {
|
|
|
4614
4633
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
4615
4634
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
4616
4635
|
workflowRunId: workflowRun.runId,
|
|
4617
|
-
deferPushUntilVerified:
|
|
4636
|
+
deferPushUntilVerified: true,
|
|
4618
4637
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
4619
4638
|
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
4620
4639
|
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
@@ -4688,16 +4707,17 @@ async function workflowSave(helpers, input) {
|
|
|
4688
4707
|
branch,
|
|
4689
4708
|
headSha: savedRootRepo.commitSha
|
|
4690
4709
|
})] : [],
|
|
4691
|
-
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4710
|
+
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).flatMap((repo) => {
|
|
4711
|
+
return hostedWorkflowsForSavedRepository(root, repo).map((workflow) => {
|
|
4712
|
+
const gate = {
|
|
4713
|
+
name: repo.name,
|
|
4714
|
+
repoPath: repo.path,
|
|
4715
|
+
workflow,
|
|
4716
|
+
branch: String(repo.branch),
|
|
4717
|
+
headSha: String(repo.commitSha)
|
|
4718
|
+
};
|
|
4719
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
4720
|
+
});
|
|
4701
4721
|
})
|
|
4702
4722
|
], "hosted", {
|
|
4703
4723
|
root,
|
|
@@ -4724,7 +4744,7 @@ async function workflowSave(helpers, input) {
|
|
|
4724
4744
|
}
|
|
4725
4745
|
return proof;
|
|
4726
4746
|
}) : saveLane === "promotion" ? (skipJournalStep(root, workflowRun.runId, "release-proof", { skippedReason: "disabled" }), { skipped: true, reason: "disabled" }) : null;
|
|
4727
|
-
const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
|
|
4747
|
+
const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" && process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
|
|
4728
4748
|
helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
|
|
4729
4749
|
const releaseSession = resolveTreeseedWorkflowSession(root);
|
|
4730
4750
|
const stagingReleasePlan = buildReleasePlanSnapshot({
|
|
@@ -4741,6 +4761,10 @@ async function workflowSave(helpers, input) {
|
|
|
4741
4761
|
lane: saveLane,
|
|
4742
4762
|
write: (line, stream) => helpers.write(line, stream)
|
|
4743
4763
|
});
|
|
4764
|
+
}) : branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? (skipJournalStep(root, workflowRun.runId, "release-candidate", { mode: releaseCandidateMode, status: "skipped" }), {
|
|
4765
|
+
mode: releaseCandidateMode,
|
|
4766
|
+
status: "skipped",
|
|
4767
|
+
reason: "release candidate rehearsal disabled"
|
|
4744
4768
|
}) : null;
|
|
4745
4769
|
let previewAction = { status: "skipped" };
|
|
4746
4770
|
if (beforeState.branchRole === "feature" && branch) {
|
|
@@ -5060,12 +5084,50 @@ async function workflowClose(helpers, input) {
|
|
|
5060
5084
|
toError("close", error);
|
|
5061
5085
|
}
|
|
5062
5086
|
}
|
|
5087
|
+
function stagingCandidateWorkflowGates(root, manifest) {
|
|
5088
|
+
const gates = [];
|
|
5089
|
+
const add = (name, repoPath, headSha, workflow, deploy = false) => {
|
|
5090
|
+
if (!workflowFileExists(repoPath, workflow)) return;
|
|
5091
|
+
const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
|
|
5092
|
+
gates.push(deploy ? hostedDeployGate(gate) : gate);
|
|
5093
|
+
};
|
|
5094
|
+
for (const pkg of manifest.packages) {
|
|
5095
|
+
const repoPath = resolve(root, pkg.path);
|
|
5096
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
|
|
5097
|
+
add(pkg.name, repoPath, pkg.commit, "verify.yml");
|
|
5098
|
+
}
|
|
5099
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5100
|
+
add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
const marketRoot = repoRoot(root);
|
|
5104
|
+
add("@treeseed/market", marketRoot, manifest.root.commit, "verify.yml");
|
|
5105
|
+
add("@treeseed/market", marketRoot, manifest.root.commit, "deploy.yml", true);
|
|
5106
|
+
return gates;
|
|
5107
|
+
}
|
|
5063
5108
|
function normalizeStageVerifyMode(value) {
|
|
5064
5109
|
return value === "local" || value === "none" ? value : "action";
|
|
5065
5110
|
}
|
|
5066
5111
|
function normalizeStageCiMode(input) {
|
|
5067
|
-
if (input.
|
|
5068
|
-
return "
|
|
5112
|
+
if (input.async === true || input.ciMode === "off") return "off";
|
|
5113
|
+
return "hosted";
|
|
5114
|
+
}
|
|
5115
|
+
function sha256File(filePath) {
|
|
5116
|
+
return existsSync(filePath) ? createHash("sha256").update(readFileSync(filePath)).digest("hex") : null;
|
|
5117
|
+
}
|
|
5118
|
+
function internalPackageDependencies(repoPath) {
|
|
5119
|
+
const packageJsonPath = resolve(repoPath, "package.json");
|
|
5120
|
+
if (!existsSync(packageJsonPath)) return [];
|
|
5121
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
5122
|
+
const names = /* @__PURE__ */ new Set();
|
|
5123
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
|
|
5124
|
+
const values = packageJson[field];
|
|
5125
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
5126
|
+
for (const name of Object.keys(values)) {
|
|
5127
|
+
if (name.startsWith("@treeseed/")) names.add(name);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
return [...names].sort();
|
|
5069
5131
|
}
|
|
5070
5132
|
function normalizeStageCleanupMode(input) {
|
|
5071
5133
|
if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
|
|
@@ -5077,6 +5139,25 @@ function stageCandidateManifestPath(root, runId) {
|
|
|
5077
5139
|
run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
|
|
5078
5140
|
};
|
|
5079
5141
|
}
|
|
5142
|
+
function readJsonFile(filePath) {
|
|
5143
|
+
if (!existsSync(filePath)) return null;
|
|
5144
|
+
try {
|
|
5145
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
5146
|
+
} catch {
|
|
5147
|
+
return null;
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
function stageCandidateAttestationBlockers(root) {
|
|
5151
|
+
const manifest = readJsonFile(stageCandidateManifestPath(root, "unused").latest);
|
|
5152
|
+
if (!manifest) return ["No staging candidate manifest is available. Run `trsd stage` and wait for staging verification and deployment workflows."];
|
|
5153
|
+
const blockers = [];
|
|
5154
|
+
if (manifest.root.commit !== headCommit(repoRoot(root))) blockers.push("The local Market staging head no longer matches the latest staged candidate.");
|
|
5155
|
+
for (const pkg of manifest.packages) {
|
|
5156
|
+
const repoPath = resolve(root, pkg.path);
|
|
5157
|
+
if (!existsSync(repoPath) || headCommit(repoPath) !== pkg.commit) blockers.push(`${pkg.name} no longer matches staged commit ${pkg.commit}.`);
|
|
5158
|
+
}
|
|
5159
|
+
return blockers;
|
|
5160
|
+
}
|
|
5080
5161
|
function writeStageCandidateManifest(root, runId, manifest) {
|
|
5081
5162
|
const paths = stageCandidateManifestPath(root, runId);
|
|
5082
5163
|
for (const filePath of [paths.latest, paths.run]) {
|
|
@@ -5198,17 +5279,24 @@ function stageConflictError(message, details) {
|
|
|
5198
5279
|
}
|
|
5199
5280
|
function createStageCandidateManifest(root, runId, branchName, plan, verification) {
|
|
5200
5281
|
const gitRoot = repoRoot(root);
|
|
5201
|
-
const packageRepos = plan.repos.filter((repo) => repo.kind === "managed"
|
|
5282
|
+
const packageRepos = plan.repos.filter((repo) => repo.kind === "managed");
|
|
5283
|
+
const rootCommit = headCommit(gitRoot);
|
|
5284
|
+
const submodules = packageRepos.map((repo) => `${relative(root, repo.path).replaceAll("\\", "/")}:${headCommit(repo.path)}`).sort();
|
|
5285
|
+
const candidateId = createHash("sha256").update(JSON.stringify({
|
|
5286
|
+
rootSha: rootCommit,
|
|
5287
|
+
submodules
|
|
5288
|
+
})).digest("hex");
|
|
5202
5289
|
return {
|
|
5203
|
-
schemaVersion:
|
|
5290
|
+
schemaVersion: 2,
|
|
5204
5291
|
kind: "treeseed.stage-candidate",
|
|
5292
|
+
candidateId,
|
|
5205
5293
|
runId,
|
|
5206
5294
|
branchName,
|
|
5207
5295
|
targetBranch: STAGING_BRANCH,
|
|
5208
5296
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5209
5297
|
root: {
|
|
5210
5298
|
repo: "@treeseed/market",
|
|
5211
|
-
commit:
|
|
5299
|
+
commit: rootCommit,
|
|
5212
5300
|
verified: verification.status === "passed" || verification.status === "skipped"
|
|
5213
5301
|
},
|
|
5214
5302
|
packages: packageRepos.map((repo) => ({
|
|
@@ -5216,6 +5304,8 @@ function createStageCandidateManifest(root, runId, branchName, plan, verificatio
|
|
|
5216
5304
|
path: repo.path,
|
|
5217
5305
|
repoKind: repo.repoKind,
|
|
5218
5306
|
commit: headCommit(repo.path),
|
|
5307
|
+
lockfileHash: sha256File(resolve(repo.path, "package-lock.json")),
|
|
5308
|
+
dependencies: internalPackageDependencies(repo.path),
|
|
5219
5309
|
remote: (() => {
|
|
5220
5310
|
try {
|
|
5221
5311
|
return originRemoteUrl(repo.path);
|
|
@@ -5507,8 +5597,17 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5507
5597
|
refs["@treeseed/market"] = rootObserved;
|
|
5508
5598
|
return { status: "verified", refs };
|
|
5509
5599
|
});
|
|
5510
|
-
const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () =>
|
|
5600
|
+
const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates(
|
|
5601
|
+
"stage",
|
|
5602
|
+
stagingCandidateWorkflowGates(root, typedManifest),
|
|
5603
|
+
"hosted",
|
|
5604
|
+
{ root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
|
|
5605
|
+
)) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
|
|
5511
5606
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
5607
|
+
for (const repo of checkedOutStagePromotionRepos(root)) {
|
|
5608
|
+
syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
|
|
5609
|
+
}
|
|
5610
|
+
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5512
5611
|
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" });
|
|
5513
5612
|
const payload = {
|
|
5514
5613
|
...basePayload,
|
|
@@ -5521,10 +5620,11 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5521
5620
|
promotion,
|
|
5522
5621
|
stagingRefs,
|
|
5523
5622
|
hostedCi,
|
|
5623
|
+
stagingGuarantees: null,
|
|
5524
5624
|
cleanup,
|
|
5525
5625
|
workspaceLinks,
|
|
5526
|
-
finalBranch:
|
|
5527
|
-
summary:
|
|
5626
|
+
finalBranch: STAGING_BRANCH,
|
|
5627
|
+
summary: ciMode === "hosted" ? `Staging candidate ${typedManifest.candidateId} passed all exact-SHA verification and deployment workflows.` : `Staging candidate ${typedManifest.candidateId} was promoted asynchronously; hosted verification is pending.`
|
|
5528
5628
|
};
|
|
5529
5629
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
5530
5630
|
return buildWorkflowResult("stage", root, payload, {
|
|
@@ -5564,7 +5664,7 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
|
|
|
5564
5664
|
resourceKind: ["release-gate"],
|
|
5565
5665
|
provider: ["treeseed"]
|
|
5566
5666
|
};
|
|
5567
|
-
const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
|
|
5667
|
+
const desiredGraph = await withContextEnv(reconcileEnv, () => compileTreeseedDesiredResourceGraph({ tenantRoot: root, target }));
|
|
5568
5668
|
const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")) && unit.unitType !== "release-gate:npm-publish" && unit.unitType !== "release-gate:image-publish" && (includeHostedReleaseGates || unit.unitType !== "release-gate:hosted-reconcile" && unit.unitType !== "release-gate:live-verify") || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
|
|
5569
5669
|
const unitsWithReleaseImageRefs = appendReleaseImageRefGitHubVariableBindings(rawUnits, input.releaseImageRefs ?? {});
|
|
5570
5670
|
const rawUnitIds = new Set(unitsWithReleaseImageRefs.map((unit) => unit.unitId));
|
|
@@ -5676,33 +5776,23 @@ function appendReleaseImageRefGitHubVariableBindings(units, releaseImageRefs) {
|
|
|
5676
5776
|
async function workflowRelease(helpers, input) {
|
|
5677
5777
|
try {
|
|
5678
5778
|
return await withContextEnv(helpers.context.env, async () => {
|
|
5679
|
-
const traceReleaseStartup = (phase) => {
|
|
5680
|
-
console.error(`[release][startup] ${phase}`);
|
|
5681
|
-
};
|
|
5682
|
-
traceReleaseStartup("resolve workspace");
|
|
5683
5779
|
const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
|
|
5684
|
-
traceReleaseStartup("resolve session");
|
|
5685
5780
|
const session = resolveTreeseedWorkflowSession(root);
|
|
5686
5781
|
const executionMode = normalizeExecutionMode(input);
|
|
5687
|
-
traceReleaseStartup("inspect root repo");
|
|
5688
5782
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
5689
|
-
traceReleaseStartup("inspect package repos");
|
|
5690
5783
|
const packageReports = createWorkspacePackageReports(root);
|
|
5691
5784
|
const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
|
|
5692
5785
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
5693
|
-
traceReleaseStartup("check auto resume");
|
|
5694
5786
|
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
|
|
5695
5787
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
5696
5788
|
const effectiveInput = autoResumeRun ? {
|
|
5697
5789
|
...autoResumeRun.input,
|
|
5698
5790
|
ciMode: input.ciMode ?? autoResumeRun.input.ciMode
|
|
5699
5791
|
} : input;
|
|
5700
|
-
traceReleaseStartup("local cleanup");
|
|
5701
5792
|
const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "release", effectiveInput);
|
|
5702
5793
|
const level = effectiveInput.bump ?? "patch";
|
|
5703
5794
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
5704
5795
|
const packageSelection = session.packageSelection;
|
|
5705
|
-
traceReleaseStartup("build release plan");
|
|
5706
5796
|
const plannedRelease = buildReleasePlanSnapshot({
|
|
5707
5797
|
root,
|
|
5708
5798
|
mode: session.mode,
|
|
@@ -5715,21 +5805,20 @@ async function workflowRelease(helpers, input) {
|
|
|
5715
5805
|
blockers: []
|
|
5716
5806
|
});
|
|
5717
5807
|
const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
|
|
5718
|
-
traceReleaseStartup("collect blockers");
|
|
5719
5808
|
const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
5720
5809
|
level,
|
|
5721
5810
|
repairVersionLine: effectiveInput.repairVersionLine === true
|
|
5722
5811
|
});
|
|
5723
5812
|
blockers.push(...collectReleaseHelperRepoBlockers(root));
|
|
5813
|
+
blockers.push(...stageCandidateAttestationBlockers(root));
|
|
5724
5814
|
const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
|
|
5725
5815
|
const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
|
|
5726
5816
|
const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
|
|
5727
|
-
|
|
5728
|
-
const plannedReadiness = collectTreeseedDeploymentReadiness({
|
|
5817
|
+
const plannedReadiness = await withContextEnv({ ...helpers.context.env, ...releaseImageRefs }, () => collectTreeseedDeploymentReadiness({
|
|
5729
5818
|
tenantRoot: root,
|
|
5730
5819
|
environment: "prod",
|
|
5731
5820
|
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
5732
|
-
});
|
|
5821
|
+
}));
|
|
5733
5822
|
blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
5734
5823
|
plannedRelease.blockers = blockers;
|
|
5735
5824
|
const releaseBasePayload = {
|
|
@@ -5769,16 +5858,13 @@ async function workflowRelease(helpers, input) {
|
|
|
5769
5858
|
releaseBasePayload
|
|
5770
5859
|
);
|
|
5771
5860
|
}
|
|
5772
|
-
traceReleaseStartup("validate blockers");
|
|
5773
5861
|
if (blockers.length > 0) {
|
|
5774
5862
|
workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
|
|
5775
5863
|
${blockers.join("\n")}`, {
|
|
5776
5864
|
details: { blockers, releasePlan: plannedRelease }
|
|
5777
5865
|
});
|
|
5778
5866
|
}
|
|
5779
|
-
traceReleaseStartup("prepare fresh release");
|
|
5780
5867
|
const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
5781
|
-
traceReleaseStartup("compute versions");
|
|
5782
5868
|
const stableVersions = new Map([
|
|
5783
5869
|
...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
|
|
5784
5870
|
...selectedVersions.entries()
|
|
@@ -5788,7 +5874,6 @@ ${blockers.join("\n")}`, {
|
|
|
5788
5874
|
...stableVersions.entries()
|
|
5789
5875
|
]);
|
|
5790
5876
|
const selectedPackageSet = new Set(selectedPackageNames);
|
|
5791
|
-
traceReleaseStartup("acquire workflow run");
|
|
5792
5877
|
const workflowRun = acquireWorkflowRun(
|
|
5793
5878
|
"release",
|
|
5794
5879
|
session,
|
|
@@ -5824,12 +5909,8 @@ ${blockers.join("\n")}`, {
|
|
|
5824
5909
|
},
|
|
5825
5910
|
{ id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5826
5911
|
{ id: "production-package-deploy-workflows", description: "Wait for production package deploy workflows before live verification", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5827
|
-
{ id: "production-hosting", description: "Reconcile and live-verify production hosted resources before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5828
|
-
{ id: "production-api-guarantees", description: "Run production API release guarantees before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5829
5912
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5830
5913
|
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5831
|
-
{ id: "production-web-live-verification", description: "Run production web live verification after root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5832
|
-
{ id: "production-final-guarantees", description: "Run final production release guarantees after all production deploys", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5833
5914
|
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5834
5915
|
{ id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
|
|
5835
5916
|
],
|
|
@@ -5960,8 +6041,6 @@ ${rendered}`);
|
|
|
5960
6041
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
5961
6042
|
}).then((workflowGates) => ({ workflowGates }));
|
|
5962
6043
|
});
|
|
5963
|
-
const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", releaseImageRefs, { liveAppId: "api" }));
|
|
5964
|
-
const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
|
|
5965
6044
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
5966
6045
|
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
5967
6046
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
@@ -5997,14 +6076,9 @@ ${rendered}`);
|
|
|
5997
6076
|
hostedDeployGate({
|
|
5998
6077
|
name: "@treeseed/market",
|
|
5999
6078
|
repoPath: repoRoot(root),
|
|
6000
|
-
workflow: "deploy
|
|
6001
|
-
branch:
|
|
6002
|
-
headSha: String(rootRelease.commit.commitSha ?? "")
|
|
6003
|
-
dispatchIfMissing: true,
|
|
6004
|
-
dispatchInputs: {
|
|
6005
|
-
environment: "prod",
|
|
6006
|
-
action_kind: "deploy_web"
|
|
6007
|
-
}
|
|
6079
|
+
workflow: "deploy.yml",
|
|
6080
|
+
branch: plannedRelease.releaseTag,
|
|
6081
|
+
headSha: String(rootRelease.commit.commitSha ?? "")
|
|
6008
6082
|
})
|
|
6009
6083
|
].filter((gate) => gate.headSha);
|
|
6010
6084
|
const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
|
|
@@ -6012,8 +6086,6 @@ ${rendered}`);
|
|
|
6012
6086
|
runId: workflowRun.runId,
|
|
6013
6087
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6014
6088
|
}).then((workflowGates) => ({ workflowGates })));
|
|
6015
|
-
const productionWebVerification = await executeJournalStep(root, workflowRun.runId, "production-web-live-verification", () => runReleaseWebLiveVerification(root, "prod", helpers, "release"));
|
|
6016
|
-
const productionFinalGuarantees = await executeJournalStep(root, workflowRun.runId, "production-final-guarantees", () => runReleaseProductionGuarantees(root, helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
|
|
6017
6089
|
const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
|
|
6018
6090
|
const packageBackMerges = selectedPackageNames.map((name) => packageRepoByName.get(name)).filter((pkg) => Boolean(pkg)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
|
|
6019
6091
|
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
@@ -6042,10 +6114,6 @@ ${rendered}`);
|
|
|
6042
6114
|
publishWait: publishWait.workflowGates,
|
|
6043
6115
|
publishedArtifacts,
|
|
6044
6116
|
productionPackageDeployWorkflows,
|
|
6045
|
-
productionHosting,
|
|
6046
|
-
productionApiGuarantees,
|
|
6047
|
-
productionWebVerification,
|
|
6048
|
-
productionFinalGuarantees,
|
|
6049
6117
|
backMerge,
|
|
6050
6118
|
workspaceLinks,
|
|
6051
6119
|
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|
|
@@ -6173,6 +6241,30 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6173
6241
|
inspection: inspectWorkflowLock(root, { scope })
|
|
6174
6242
|
}));
|
|
6175
6243
|
const lock = locks.find((entry) => entry.inspection.active)?.inspection ?? locks.find((entry) => entry.inspection.stale)?.inspection ?? locks[0].inspection;
|
|
6244
|
+
const hasActiveLock = locks.some((entry) => entry.inspection.active);
|
|
6245
|
+
const orphanedRunningRuns = hasActiveLock ? [] : listWorkflowRunJournals(root).filter((journal) => journal.status === "running");
|
|
6246
|
+
const prunedOrphanedRuns = input.pruneStale === true ? orphanedRunningRuns.map((journal) => {
|
|
6247
|
+
const classification = {
|
|
6248
|
+
state: "stale",
|
|
6249
|
+
reasons: ["workflow journal was left running without an active workflow lock"],
|
|
6250
|
+
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6251
|
+
};
|
|
6252
|
+
archiveWorkflowRun(root, journal.runId, classification);
|
|
6253
|
+
return { runId: journal.runId, command: journal.command, status: journal.status, classification };
|
|
6254
|
+
}) : orphanedRunningRuns.map((journal) => {
|
|
6255
|
+
updateWorkflowRunJournal(root, journal.runId, (current) => ({
|
|
6256
|
+
...current,
|
|
6257
|
+
status: "failed",
|
|
6258
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6259
|
+
failure: {
|
|
6260
|
+
code: "interrupted",
|
|
6261
|
+
message: "Workflow process ended without finalizing its journal.",
|
|
6262
|
+
details: { recovery: { resumable: current.resumable, runId: current.runId, resumeCommand: `treeseed resume ${current.runId}` } },
|
|
6263
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6264
|
+
}
|
|
6265
|
+
}));
|
|
6266
|
+
return null;
|
|
6267
|
+
}).filter((entry) => entry !== null);
|
|
6176
6268
|
const journals = listWorkflowRunJournals(root);
|
|
6177
6269
|
const session = resolveTreeseedWorkflowSession(root);
|
|
6178
6270
|
const currentHeads = Object.fromEntries(
|
|
@@ -6248,7 +6340,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6248
6340
|
interruptedRuns,
|
|
6249
6341
|
staleRuns,
|
|
6250
6342
|
obsoleteRuns,
|
|
6251
|
-
prunedRuns,
|
|
6343
|
+
prunedRuns: [...prunedOrphanedRuns, ...prunedRuns],
|
|
6252
6344
|
markedObsoleteRun,
|
|
6253
6345
|
selectedRun,
|
|
6254
6346
|
runCount: journals.length
|