@treeseed/sdk 0.12.44 → 0.12.46
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 +8 -5
- 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 +88 -19
- package/dist/operations/services/workspace-dependency-mode.js +4 -0
- package/dist/platform/desired-state.js +3 -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/reconcile/providers/release-private.d.ts +10 -0
- package/dist/reconcile/providers/release-private.js +45 -1
- 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 +252 -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;
|
|
@@ -1244,6 +1222,14 @@ function remoteTagCommit(repoDir, tagName) {
|
|
|
1244
1222
|
const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
|
|
1245
1223
|
return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
|
|
1246
1224
|
}
|
|
1225
|
+
function releaseTagExists(repoDir, tagName) {
|
|
1226
|
+
if (gitObjectCommit(repoDir, tagName)) return true;
|
|
1227
|
+
try {
|
|
1228
|
+
return remoteTagCommit(repoDir, tagName) !== null;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1247
1233
|
function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
1248
1234
|
const localCommit = gitObjectCommit(repoDir, tagName);
|
|
1249
1235
|
if (localCommit && localCommit !== commitSha) {
|
|
@@ -1360,7 +1346,7 @@ function defaultCiWorkflows(kind, branch) {
|
|
|
1360
1346
|
return ["verify.yml"];
|
|
1361
1347
|
}
|
|
1362
1348
|
if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
|
|
1363
|
-
return ["deploy
|
|
1349
|
+
return ["deploy.yml"];
|
|
1364
1350
|
}
|
|
1365
1351
|
return ["verify.yml"];
|
|
1366
1352
|
}
|
|
@@ -1710,34 +1696,50 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
1710
1696
|
function workflowFileExists(repoPath, workflow) {
|
|
1711
1697
|
return existsSync(resolve(repoPath, ".github", "workflows", workflow));
|
|
1712
1698
|
}
|
|
1713
|
-
function
|
|
1714
|
-
const
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1699
|
+
function hostedWorkflowsForSavedRepository(root, repo, adapter) {
|
|
1700
|
+
const workflows = [];
|
|
1701
|
+
const addWorkflow = (workflow) => {
|
|
1702
|
+
if (!workflow) return;
|
|
1703
|
+
const normalized = workflow.trim().replace(/^\.github\/workflows\//u, "");
|
|
1704
|
+
if (normalized && !workflows.includes(normalized)) {
|
|
1705
|
+
workflows.push(normalized);
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1717
1708
|
if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
|
|
1718
|
-
|
|
1709
|
+
addWorkflow("deploy.yml");
|
|
1710
|
+
} else {
|
|
1711
|
+
const fallbackAdapter = adapter ?? new Map(discoverTreeseedPackageAdapters(root).map((entry) => [resolve(entry.dir), entry])).get(resolve(repo.path));
|
|
1712
|
+
const adapterWorkflow = packageHostedVerifyWorkflow(fallbackAdapter);
|
|
1713
|
+
addWorkflow(adapterWorkflow);
|
|
1719
1714
|
}
|
|
1720
|
-
if (workflowFileExists(repo.path, "verify.yml"))
|
|
1721
|
-
return
|
|
1715
|
+
if (workflows.length === 0 && workflowFileExists(repo.path, "verify.yml")) addWorkflow("verify.yml");
|
|
1716
|
+
return workflows;
|
|
1722
1717
|
}
|
|
1723
1718
|
function gatesForSavedRepositoryReports(root, reports) {
|
|
1719
|
+
const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
|
|
1724
1720
|
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
|
-
|
|
1721
|
+
const adapter = adapterByPath.get(resolve(repo.path));
|
|
1722
|
+
return hostedWorkflowsForSavedRepository(root, repo, adapter).map((workflow) => {
|
|
1723
|
+
const gate = {
|
|
1724
|
+
name: repo.name,
|
|
1725
|
+
repoPath: repo.path,
|
|
1726
|
+
workflow,
|
|
1727
|
+
branch: String(repo.branch),
|
|
1728
|
+
headSha: String(repo.commitSha),
|
|
1729
|
+
...packageHostedVerifyTimeoutSeconds(adapter) ? { timeoutSeconds: packageHostedVerifyTimeoutSeconds(adapter) } : {}
|
|
1730
|
+
};
|
|
1731
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
1732
|
+
});
|
|
1735
1733
|
});
|
|
1736
1734
|
}
|
|
1737
1735
|
function packageHostedVerifyWorkflow(adapter) {
|
|
1738
1736
|
const workflow = adapter?.metadata?.hostedVerifyWorkflow;
|
|
1739
1737
|
return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
|
|
1740
1738
|
}
|
|
1739
|
+
function packageHostedVerifyTimeoutSeconds(adapter) {
|
|
1740
|
+
const timeoutSeconds = adapter?.metadata?.hostedVerifyTimeoutSeconds;
|
|
1741
|
+
return typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) : null;
|
|
1742
|
+
}
|
|
1741
1743
|
function gateForSavedRootReport(report, branch, scope) {
|
|
1742
1744
|
if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
|
|
1743
1745
|
return [];
|
|
@@ -2148,7 +2150,7 @@ function validateStagingWorkflowContracts(root) {
|
|
|
2148
2150
|
return;
|
|
2149
2151
|
}
|
|
2150
2152
|
const missing = [];
|
|
2151
|
-
for (const fileName of ["verify.yml", "deploy
|
|
2153
|
+
for (const fileName of ["verify.yml", "deploy.yml"]) {
|
|
2152
2154
|
if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
|
|
2153
2155
|
missing.push(fileName);
|
|
2154
2156
|
}
|
|
@@ -2615,7 +2617,17 @@ async function workflowReleaseCandidate(helpers, input = {}) {
|
|
|
2615
2617
|
}
|
|
2616
2618
|
}
|
|
2617
2619
|
function buildReleasePlanSnapshot(input) {
|
|
2618
|
-
const
|
|
2620
|
+
const publishablePackageNames = new Set(
|
|
2621
|
+
discoverTreeseedPackageAdapters(input.root).filter((adapter) => adapter.capabilities.publish).map((adapter) => adapter.id)
|
|
2622
|
+
);
|
|
2623
|
+
const selectedPackageNames = new Set(
|
|
2624
|
+
input.packageSelection.selected.filter((name) => publishablePackageNames.has(name))
|
|
2625
|
+
);
|
|
2626
|
+
const publishablePackageSelection = {
|
|
2627
|
+
changed: input.packageSelection.changed.filter((name) => selectedPackageNames.has(name)),
|
|
2628
|
+
dependents: input.packageSelection.dependents.filter((name) => selectedPackageNames.has(name)),
|
|
2629
|
+
selected: [...selectedPackageNames]
|
|
2630
|
+
};
|
|
2619
2631
|
const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
|
|
2620
2632
|
const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
|
|
2621
2633
|
if (input.repairVersionLine !== true) {
|
|
@@ -2625,8 +2637,16 @@ function buildReleasePlanSnapshot(input) {
|
|
|
2625
2637
|
versionPlan.versions.set(adapter.id, incrementVersion(adapter.version, input.level));
|
|
2626
2638
|
}
|
|
2627
2639
|
}
|
|
2640
|
+
for (const adapter of discoverTreeseedPackageAdapters(input.root)) {
|
|
2641
|
+
let version = versionPlan.versions.get(adapter.id);
|
|
2642
|
+
if (!version) continue;
|
|
2643
|
+
while (releaseTagExists(adapter.dir, version)) {
|
|
2644
|
+
version = incrementVersion(version, input.level);
|
|
2645
|
+
}
|
|
2646
|
+
versionPlan.versions.set(adapter.id, version);
|
|
2647
|
+
}
|
|
2628
2648
|
const plannedSelected = orderReleasePackageNames([...versionPlan.selected].filter((name) => versionPlan.versions.has(name)));
|
|
2629
|
-
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(
|
|
2649
|
+
const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(publishablePackageSelection.changed.filter((name) => plannedSelected.includes(name))));
|
|
2630
2650
|
const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
|
|
2631
2651
|
const plannedPackageSelection = {
|
|
2632
2652
|
changed: plannedChanged,
|
|
@@ -3008,6 +3028,9 @@ async function collectPublishedReleaseArtifactChecks(selectedVersions) {
|
|
|
3008
3028
|
return checks;
|
|
3009
3029
|
}
|
|
3010
3030
|
async function verifyPublishedReleaseArtifacts(selectedVersions) {
|
|
3031
|
+
if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
|
|
3032
|
+
return { checks: [] };
|
|
3033
|
+
}
|
|
3011
3034
|
let checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
|
|
3012
3035
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
3013
3036
|
while (checks.some((check) => !check.ok) && Date.now() < deadline) {
|
|
@@ -3766,8 +3789,9 @@ async function workflowSwitch(helpers, input) {
|
|
|
3766
3789
|
});
|
|
3767
3790
|
const session = resolveTreeseedWorkflowSession(root);
|
|
3768
3791
|
const preview = input.preview === true;
|
|
3792
|
+
const adoptChanges = input.adoptChanges === true;
|
|
3769
3793
|
const executionMode = normalizeExecutionMode(input);
|
|
3770
|
-
if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
3794
|
+
if (executionMode !== "plan" && !adoptChanges && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
3771
3795
|
const managed = ensureManagedWorkflowWorktree({
|
|
3772
3796
|
root,
|
|
3773
3797
|
branchName,
|
|
@@ -3820,7 +3844,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3820
3844
|
previewRequested: preview,
|
|
3821
3845
|
worktreeMode: input.worktreeMode ?? "auto",
|
|
3822
3846
|
worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
|
|
3823
|
-
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
3847
|
+
blockers: !adoptChanges && dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
3824
3848
|
plannedSteps: [
|
|
3825
3849
|
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
3826
3850
|
...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
|
|
@@ -3837,7 +3861,17 @@ async function workflowSwitch(helpers, input) {
|
|
|
3837
3861
|
}
|
|
3838
3862
|
);
|
|
3839
3863
|
}
|
|
3840
|
-
if (
|
|
3864
|
+
if (adoptChanges) {
|
|
3865
|
+
const reports = [rootRepo, ...packageReports];
|
|
3866
|
+
const existingTargets = reports.filter((report) => branchExists(report.path, branchName) || remoteBranchExists(report.path, branchName));
|
|
3867
|
+
if (existingTargets.length > 0) {
|
|
3868
|
+
workflowError("switch", "validation_failed", `--adopt-changes requires a new branch; ${branchName} already exists in ${existingTargets.map((report) => report.name).join(", ")}.`);
|
|
3869
|
+
}
|
|
3870
|
+
const unsafeDirtyRepos = reports.filter((report) => report.dirty && currentBranch(report.path) !== STAGING_BRANCH);
|
|
3871
|
+
if (unsafeDirtyRepos.length > 0) {
|
|
3872
|
+
workflowError("switch", "validation_failed", `--adopt-changes only accepts dirty staging repositories: ${unsafeDirtyRepos.map((report) => report.name).join(", ")}.`);
|
|
3873
|
+
}
|
|
3874
|
+
} else if (mode === "recursive-workspace") {
|
|
3841
3875
|
assertWorkspaceClean(root);
|
|
3842
3876
|
assertSessionBranchSafety("switch", session);
|
|
3843
3877
|
} else {
|
|
@@ -3846,7 +3880,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3846
3880
|
const workflowRun = acquireWorkflowRun(
|
|
3847
3881
|
"switch",
|
|
3848
3882
|
session,
|
|
3849
|
-
{ branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
|
|
3883
|
+
{ branch: branchName, preview, adoptChanges, worktreeMode: input.worktreeMode ?? "auto" },
|
|
3850
3884
|
[
|
|
3851
3885
|
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
3852
3886
|
...packageReports.map((report) => ({
|
|
@@ -3867,7 +3901,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3867
3901
|
root,
|
|
3868
3902
|
workflowRun.runId,
|
|
3869
3903
|
"switch-root",
|
|
3870
|
-
() => checkoutTaskBranchFromStaging(repoDir, branchName, {
|
|
3904
|
+
() => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(repoDir, branchName, {
|
|
3871
3905
|
createIfMissing: input.createIfMissing !== false,
|
|
3872
3906
|
pushIfCreated: true
|
|
3873
3907
|
})
|
|
@@ -3886,7 +3920,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
3886
3920
|
root,
|
|
3887
3921
|
workflowRun.runId,
|
|
3888
3922
|
`switch-${report.name}`,
|
|
3889
|
-
() => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
|
|
3923
|
+
() => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(managedRepo.dir, branchName, {
|
|
3890
3924
|
createIfMissing: input.createIfMissing !== false,
|
|
3891
3925
|
pushIfCreated: false
|
|
3892
3926
|
})
|
|
@@ -3925,7 +3959,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
3925
3959
|
workspaceLinks,
|
|
3926
3960
|
...worktreePayload(root, input.worktreeMode),
|
|
3927
3961
|
preconditions: {
|
|
3928
|
-
cleanWorktreeRequired:
|
|
3962
|
+
cleanWorktreeRequired: !adoptChanges,
|
|
3963
|
+
adoptedDirtyStagingChanges: adoptChanges,
|
|
3929
3964
|
baseBranch: STAGING_BRANCH
|
|
3930
3965
|
}
|
|
3931
3966
|
};
|
|
@@ -4427,8 +4462,8 @@ async function workflowSave(helpers, input) {
|
|
|
4427
4462
|
const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "save", effectiveInput);
|
|
4428
4463
|
const message = String(effectiveInput.message ?? "").trim();
|
|
4429
4464
|
const saveLane = normalizeSaveLane(effectiveInput.lane);
|
|
4430
|
-
const saveCiMode =
|
|
4431
|
-
const releaseCandidateMode =
|
|
4465
|
+
const saveCiMode = "off";
|
|
4466
|
+
const releaseCandidateMode = "skip";
|
|
4432
4467
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
4433
4468
|
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
4434
4469
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
@@ -4614,7 +4649,7 @@ async function workflowSave(helpers, input) {
|
|
|
4614
4649
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
4615
4650
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
4616
4651
|
workflowRunId: workflowRun.runId,
|
|
4617
|
-
deferPushUntilVerified:
|
|
4652
|
+
deferPushUntilVerified: false,
|
|
4618
4653
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
4619
4654
|
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
4620
4655
|
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
@@ -4688,16 +4723,17 @@ async function workflowSave(helpers, input) {
|
|
|
4688
4723
|
branch,
|
|
4689
4724
|
headSha: savedRootRepo.commitSha
|
|
4690
4725
|
})] : [],
|
|
4691
|
-
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4726
|
+
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).flatMap((repo) => {
|
|
4727
|
+
return hostedWorkflowsForSavedRepository(root, repo).map((workflow) => {
|
|
4728
|
+
const gate = {
|
|
4729
|
+
name: repo.name,
|
|
4730
|
+
repoPath: repo.path,
|
|
4731
|
+
workflow,
|
|
4732
|
+
branch: String(repo.branch),
|
|
4733
|
+
headSha: String(repo.commitSha)
|
|
4734
|
+
};
|
|
4735
|
+
return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
|
|
4736
|
+
});
|
|
4701
4737
|
})
|
|
4702
4738
|
], "hosted", {
|
|
4703
4739
|
root,
|
|
@@ -4724,7 +4760,7 @@ async function workflowSave(helpers, input) {
|
|
|
4724
4760
|
}
|
|
4725
4761
|
return proof;
|
|
4726
4762
|
}) : 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", () => {
|
|
4763
|
+
const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" && process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
|
|
4728
4764
|
helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
|
|
4729
4765
|
const releaseSession = resolveTreeseedWorkflowSession(root);
|
|
4730
4766
|
const stagingReleasePlan = buildReleasePlanSnapshot({
|
|
@@ -4741,6 +4777,10 @@ async function workflowSave(helpers, input) {
|
|
|
4741
4777
|
lane: saveLane,
|
|
4742
4778
|
write: (line, stream) => helpers.write(line, stream)
|
|
4743
4779
|
});
|
|
4780
|
+
}) : branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? (skipJournalStep(root, workflowRun.runId, "release-candidate", { mode: releaseCandidateMode, status: "skipped" }), {
|
|
4781
|
+
mode: releaseCandidateMode,
|
|
4782
|
+
status: "skipped",
|
|
4783
|
+
reason: "release candidate rehearsal disabled"
|
|
4744
4784
|
}) : null;
|
|
4745
4785
|
let previewAction = { status: "skipped" };
|
|
4746
4786
|
if (beforeState.branchRole === "feature" && branch) {
|
|
@@ -5060,12 +5100,50 @@ async function workflowClose(helpers, input) {
|
|
|
5060
5100
|
toError("close", error);
|
|
5061
5101
|
}
|
|
5062
5102
|
}
|
|
5103
|
+
function stagingCandidateWorkflowGates(root, manifest) {
|
|
5104
|
+
const gates = [];
|
|
5105
|
+
const add = (name, repoPath, headSha, workflow, deploy = false) => {
|
|
5106
|
+
if (!workflowFileExists(repoPath, workflow)) return;
|
|
5107
|
+
const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
|
|
5108
|
+
gates.push(deploy ? hostedDeployGate(gate) : gate);
|
|
5109
|
+
};
|
|
5110
|
+
for (const pkg of manifest.packages) {
|
|
5111
|
+
const repoPath = resolve(root, pkg.path);
|
|
5112
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
|
|
5113
|
+
add(pkg.name, repoPath, pkg.commit, "verify.yml");
|
|
5114
|
+
}
|
|
5115
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5116
|
+
add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
const marketRoot = repoRoot(root);
|
|
5120
|
+
add("@treeseed/market", marketRoot, manifest.root.commit, "verify.yml");
|
|
5121
|
+
add("@treeseed/market", marketRoot, manifest.root.commit, "deploy.yml", true);
|
|
5122
|
+
return gates;
|
|
5123
|
+
}
|
|
5063
5124
|
function normalizeStageVerifyMode(value) {
|
|
5064
5125
|
return value === "local" || value === "none" ? value : "action";
|
|
5065
5126
|
}
|
|
5066
5127
|
function normalizeStageCiMode(input) {
|
|
5067
|
-
if (input.
|
|
5068
|
-
return "
|
|
5128
|
+
if (input.async === true || input.ciMode === "off") return "off";
|
|
5129
|
+
return "hosted";
|
|
5130
|
+
}
|
|
5131
|
+
function sha256File(filePath) {
|
|
5132
|
+
return existsSync(filePath) ? createHash("sha256").update(readFileSync(filePath)).digest("hex") : null;
|
|
5133
|
+
}
|
|
5134
|
+
function internalPackageDependencies(repoPath) {
|
|
5135
|
+
const packageJsonPath = resolve(repoPath, "package.json");
|
|
5136
|
+
if (!existsSync(packageJsonPath)) return [];
|
|
5137
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
5138
|
+
const names = /* @__PURE__ */ new Set();
|
|
5139
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
|
|
5140
|
+
const values = packageJson[field];
|
|
5141
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
5142
|
+
for (const name of Object.keys(values)) {
|
|
5143
|
+
if (name.startsWith("@treeseed/")) names.add(name);
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
return [...names].sort();
|
|
5069
5147
|
}
|
|
5070
5148
|
function normalizeStageCleanupMode(input) {
|
|
5071
5149
|
if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
|
|
@@ -5077,6 +5155,25 @@ function stageCandidateManifestPath(root, runId) {
|
|
|
5077
5155
|
run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
|
|
5078
5156
|
};
|
|
5079
5157
|
}
|
|
5158
|
+
function readJsonFile(filePath) {
|
|
5159
|
+
if (!existsSync(filePath)) return null;
|
|
5160
|
+
try {
|
|
5161
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
5162
|
+
} catch {
|
|
5163
|
+
return null;
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
function stageCandidateAttestationBlockers(root) {
|
|
5167
|
+
const manifest = readJsonFile(stageCandidateManifestPath(root, "unused").latest);
|
|
5168
|
+
if (!manifest) return ["No staging candidate manifest is available. Run `trsd stage` and wait for staging verification and deployment workflows."];
|
|
5169
|
+
const blockers = [];
|
|
5170
|
+
if (manifest.root.commit !== headCommit(repoRoot(root))) blockers.push("The local Market staging head no longer matches the latest staged candidate.");
|
|
5171
|
+
for (const pkg of manifest.packages) {
|
|
5172
|
+
const repoPath = resolve(root, pkg.path);
|
|
5173
|
+
if (!existsSync(repoPath) || headCommit(repoPath) !== pkg.commit) blockers.push(`${pkg.name} no longer matches staged commit ${pkg.commit}.`);
|
|
5174
|
+
}
|
|
5175
|
+
return blockers;
|
|
5176
|
+
}
|
|
5080
5177
|
function writeStageCandidateManifest(root, runId, manifest) {
|
|
5081
5178
|
const paths = stageCandidateManifestPath(root, runId);
|
|
5082
5179
|
for (const filePath of [paths.latest, paths.run]) {
|
|
@@ -5198,17 +5295,24 @@ function stageConflictError(message, details) {
|
|
|
5198
5295
|
}
|
|
5199
5296
|
function createStageCandidateManifest(root, runId, branchName, plan, verification) {
|
|
5200
5297
|
const gitRoot = repoRoot(root);
|
|
5201
|
-
const packageRepos = plan.repos.filter((repo) => repo.kind === "managed"
|
|
5298
|
+
const packageRepos = plan.repos.filter((repo) => repo.kind === "managed");
|
|
5299
|
+
const rootCommit = headCommit(gitRoot);
|
|
5300
|
+
const submodules = packageRepos.map((repo) => `${relative(root, repo.path).replaceAll("\\", "/")}:${headCommit(repo.path)}`).sort();
|
|
5301
|
+
const candidateId = createHash("sha256").update(JSON.stringify({
|
|
5302
|
+
rootSha: rootCommit,
|
|
5303
|
+
submodules
|
|
5304
|
+
})).digest("hex");
|
|
5202
5305
|
return {
|
|
5203
|
-
schemaVersion:
|
|
5306
|
+
schemaVersion: 2,
|
|
5204
5307
|
kind: "treeseed.stage-candidate",
|
|
5308
|
+
candidateId,
|
|
5205
5309
|
runId,
|
|
5206
5310
|
branchName,
|
|
5207
5311
|
targetBranch: STAGING_BRANCH,
|
|
5208
5312
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5209
5313
|
root: {
|
|
5210
5314
|
repo: "@treeseed/market",
|
|
5211
|
-
commit:
|
|
5315
|
+
commit: rootCommit,
|
|
5212
5316
|
verified: verification.status === "passed" || verification.status === "skipped"
|
|
5213
5317
|
},
|
|
5214
5318
|
packages: packageRepos.map((repo) => ({
|
|
@@ -5216,6 +5320,8 @@ function createStageCandidateManifest(root, runId, branchName, plan, verificatio
|
|
|
5216
5320
|
path: repo.path,
|
|
5217
5321
|
repoKind: repo.repoKind,
|
|
5218
5322
|
commit: headCommit(repo.path),
|
|
5323
|
+
lockfileHash: sha256File(resolve(repo.path, "package-lock.json")),
|
|
5324
|
+
dependencies: internalPackageDependencies(repo.path),
|
|
5219
5325
|
remote: (() => {
|
|
5220
5326
|
try {
|
|
5221
5327
|
return originRemoteUrl(repo.path);
|
|
@@ -5507,8 +5613,17 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5507
5613
|
refs["@treeseed/market"] = rootObserved;
|
|
5508
5614
|
return { status: "verified", refs };
|
|
5509
5615
|
});
|
|
5510
|
-
const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () =>
|
|
5616
|
+
const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates(
|
|
5617
|
+
"stage",
|
|
5618
|
+
stagingCandidateWorkflowGates(root, typedManifest),
|
|
5619
|
+
"hosted",
|
|
5620
|
+
{ root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
|
|
5621
|
+
)) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
|
|
5511
5622
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
5623
|
+
for (const repo of checkedOutStagePromotionRepos(root)) {
|
|
5624
|
+
syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
|
|
5625
|
+
}
|
|
5626
|
+
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5512
5627
|
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
5628
|
const payload = {
|
|
5514
5629
|
...basePayload,
|
|
@@ -5521,10 +5636,11 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5521
5636
|
promotion,
|
|
5522
5637
|
stagingRefs,
|
|
5523
5638
|
hostedCi,
|
|
5639
|
+
stagingGuarantees: null,
|
|
5524
5640
|
cleanup,
|
|
5525
5641
|
workspaceLinks,
|
|
5526
|
-
finalBranch:
|
|
5527
|
-
summary:
|
|
5642
|
+
finalBranch: STAGING_BRANCH,
|
|
5643
|
+
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
5644
|
};
|
|
5529
5645
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
5530
5646
|
return buildWorkflowResult("stage", root, payload, {
|
|
@@ -5564,7 +5680,7 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
|
|
|
5564
5680
|
resourceKind: ["release-gate"],
|
|
5565
5681
|
provider: ["treeseed"]
|
|
5566
5682
|
};
|
|
5567
|
-
const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
|
|
5683
|
+
const desiredGraph = await withContextEnv(reconcileEnv, () => compileTreeseedDesiredResourceGraph({ tenantRoot: root, target }));
|
|
5568
5684
|
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
5685
|
const unitsWithReleaseImageRefs = appendReleaseImageRefGitHubVariableBindings(rawUnits, input.releaseImageRefs ?? {});
|
|
5570
5686
|
const rawUnitIds = new Set(unitsWithReleaseImageRefs.map((unit) => unit.unitId));
|
|
@@ -5676,33 +5792,23 @@ function appendReleaseImageRefGitHubVariableBindings(units, releaseImageRefs) {
|
|
|
5676
5792
|
async function workflowRelease(helpers, input) {
|
|
5677
5793
|
try {
|
|
5678
5794
|
return await withContextEnv(helpers.context.env, async () => {
|
|
5679
|
-
const traceReleaseStartup = (phase) => {
|
|
5680
|
-
console.error(`[release][startup] ${phase}`);
|
|
5681
|
-
};
|
|
5682
|
-
traceReleaseStartup("resolve workspace");
|
|
5683
5795
|
const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
|
|
5684
|
-
traceReleaseStartup("resolve session");
|
|
5685
5796
|
const session = resolveTreeseedWorkflowSession(root);
|
|
5686
5797
|
const executionMode = normalizeExecutionMode(input);
|
|
5687
|
-
traceReleaseStartup("inspect root repo");
|
|
5688
5798
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
5689
|
-
traceReleaseStartup("inspect package repos");
|
|
5690
5799
|
const packageReports = createWorkspacePackageReports(root);
|
|
5691
5800
|
const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
|
|
5692
5801
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
5693
|
-
traceReleaseStartup("check auto resume");
|
|
5694
5802
|
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
|
|
5695
5803
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
5696
5804
|
const effectiveInput = autoResumeRun ? {
|
|
5697
5805
|
...autoResumeRun.input,
|
|
5698
5806
|
ciMode: input.ciMode ?? autoResumeRun.input.ciMode
|
|
5699
5807
|
} : input;
|
|
5700
|
-
traceReleaseStartup("local cleanup");
|
|
5701
5808
|
const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "release", effectiveInput);
|
|
5702
5809
|
const level = effectiveInput.bump ?? "patch";
|
|
5703
5810
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
5704
5811
|
const packageSelection = session.packageSelection;
|
|
5705
|
-
traceReleaseStartup("build release plan");
|
|
5706
5812
|
const plannedRelease = buildReleasePlanSnapshot({
|
|
5707
5813
|
root,
|
|
5708
5814
|
mode: session.mode,
|
|
@@ -5715,21 +5821,20 @@ async function workflowRelease(helpers, input) {
|
|
|
5715
5821
|
blockers: []
|
|
5716
5822
|
});
|
|
5717
5823
|
const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
|
|
5718
|
-
traceReleaseStartup("collect blockers");
|
|
5719
5824
|
const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
5720
5825
|
level,
|
|
5721
5826
|
repairVersionLine: effectiveInput.repairVersionLine === true
|
|
5722
5827
|
});
|
|
5723
5828
|
blockers.push(...collectReleaseHelperRepoBlockers(root));
|
|
5829
|
+
blockers.push(...stageCandidateAttestationBlockers(root));
|
|
5724
5830
|
const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
|
|
5725
5831
|
const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
|
|
5726
5832
|
const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
|
|
5727
|
-
|
|
5728
|
-
const plannedReadiness = collectTreeseedDeploymentReadiness({
|
|
5833
|
+
const plannedReadiness = await withContextEnv({ ...helpers.context.env, ...releaseImageRefs }, () => collectTreeseedDeploymentReadiness({
|
|
5729
5834
|
tenantRoot: root,
|
|
5730
5835
|
environment: "prod",
|
|
5731
5836
|
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
5732
|
-
});
|
|
5837
|
+
}));
|
|
5733
5838
|
blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
|
|
5734
5839
|
plannedRelease.blockers = blockers;
|
|
5735
5840
|
const releaseBasePayload = {
|
|
@@ -5769,16 +5874,13 @@ async function workflowRelease(helpers, input) {
|
|
|
5769
5874
|
releaseBasePayload
|
|
5770
5875
|
);
|
|
5771
5876
|
}
|
|
5772
|
-
traceReleaseStartup("validate blockers");
|
|
5773
5877
|
if (blockers.length > 0) {
|
|
5774
5878
|
workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
|
|
5775
5879
|
${blockers.join("\n")}`, {
|
|
5776
5880
|
details: { blockers, releasePlan: plannedRelease }
|
|
5777
5881
|
});
|
|
5778
5882
|
}
|
|
5779
|
-
traceReleaseStartup("prepare fresh release");
|
|
5780
5883
|
const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
5781
|
-
traceReleaseStartup("compute versions");
|
|
5782
5884
|
const stableVersions = new Map([
|
|
5783
5885
|
...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
|
|
5784
5886
|
...selectedVersions.entries()
|
|
@@ -5788,7 +5890,6 @@ ${blockers.join("\n")}`, {
|
|
|
5788
5890
|
...stableVersions.entries()
|
|
5789
5891
|
]);
|
|
5790
5892
|
const selectedPackageSet = new Set(selectedPackageNames);
|
|
5791
|
-
traceReleaseStartup("acquire workflow run");
|
|
5792
5893
|
const workflowRun = acquireWorkflowRun(
|
|
5793
5894
|
"release",
|
|
5794
5895
|
session,
|
|
@@ -5824,12 +5925,8 @@ ${blockers.join("\n")}`, {
|
|
|
5824
5925
|
},
|
|
5825
5926
|
{ 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
5927
|
{ 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
5928
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5830
5929
|
{ 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
5930
|
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5834
5931
|
{ id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
|
|
5835
5932
|
],
|
|
@@ -5960,8 +6057,6 @@ ${rendered}`);
|
|
|
5960
6057
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
5961
6058
|
}).then((workflowGates) => ({ workflowGates }));
|
|
5962
6059
|
});
|
|
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
6060
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
5966
6061
|
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
5967
6062
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
@@ -5997,14 +6092,9 @@ ${rendered}`);
|
|
|
5997
6092
|
hostedDeployGate({
|
|
5998
6093
|
name: "@treeseed/market",
|
|
5999
6094
|
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
|
-
}
|
|
6095
|
+
workflow: "deploy.yml",
|
|
6096
|
+
branch: plannedRelease.releaseTag,
|
|
6097
|
+
headSha: String(rootRelease.commit.commitSha ?? "")
|
|
6008
6098
|
})
|
|
6009
6099
|
].filter((gate) => gate.headSha);
|
|
6010
6100
|
const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
|
|
@@ -6012,8 +6102,6 @@ ${rendered}`);
|
|
|
6012
6102
|
runId: workflowRun.runId,
|
|
6013
6103
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6014
6104
|
}).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
6105
|
const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
|
|
6018
6106
|
const packageBackMerges = selectedPackageNames.map((name) => packageRepoByName.get(name)).filter((pkg) => Boolean(pkg)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
|
|
6019
6107
|
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
@@ -6042,10 +6130,6 @@ ${rendered}`);
|
|
|
6042
6130
|
publishWait: publishWait.workflowGates,
|
|
6043
6131
|
publishedArtifacts,
|
|
6044
6132
|
productionPackageDeployWorkflows,
|
|
6045
|
-
productionHosting,
|
|
6046
|
-
productionApiGuarantees,
|
|
6047
|
-
productionWebVerification,
|
|
6048
|
-
productionFinalGuarantees,
|
|
6049
6133
|
backMerge,
|
|
6050
6134
|
workspaceLinks,
|
|
6051
6135
|
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|
|
@@ -6173,6 +6257,30 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6173
6257
|
inspection: inspectWorkflowLock(root, { scope })
|
|
6174
6258
|
}));
|
|
6175
6259
|
const lock = locks.find((entry) => entry.inspection.active)?.inspection ?? locks.find((entry) => entry.inspection.stale)?.inspection ?? locks[0].inspection;
|
|
6260
|
+
const hasActiveLock = locks.some((entry) => entry.inspection.active);
|
|
6261
|
+
const orphanedRunningRuns = hasActiveLock ? [] : listWorkflowRunJournals(root).filter((journal) => journal.status === "running");
|
|
6262
|
+
const prunedOrphanedRuns = input.pruneStale === true ? orphanedRunningRuns.map((journal) => {
|
|
6263
|
+
const classification = {
|
|
6264
|
+
state: "stale",
|
|
6265
|
+
reasons: ["workflow journal was left running without an active workflow lock"],
|
|
6266
|
+
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6267
|
+
};
|
|
6268
|
+
archiveWorkflowRun(root, journal.runId, classification);
|
|
6269
|
+
return { runId: journal.runId, command: journal.command, status: journal.status, classification };
|
|
6270
|
+
}) : orphanedRunningRuns.map((journal) => {
|
|
6271
|
+
updateWorkflowRunJournal(root, journal.runId, (current) => ({
|
|
6272
|
+
...current,
|
|
6273
|
+
status: "failed",
|
|
6274
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6275
|
+
failure: {
|
|
6276
|
+
code: "interrupted",
|
|
6277
|
+
message: "Workflow process ended without finalizing its journal.",
|
|
6278
|
+
details: { recovery: { resumable: current.resumable, runId: current.runId, resumeCommand: `treeseed resume ${current.runId}` } },
|
|
6279
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6280
|
+
}
|
|
6281
|
+
}));
|
|
6282
|
+
return null;
|
|
6283
|
+
}).filter((entry) => entry !== null);
|
|
6176
6284
|
const journals = listWorkflowRunJournals(root);
|
|
6177
6285
|
const session = resolveTreeseedWorkflowSession(root);
|
|
6178
6286
|
const currentHeads = Object.fromEntries(
|
|
@@ -6248,7 +6356,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6248
6356
|
interruptedRuns,
|
|
6249
6357
|
staleRuns,
|
|
6250
6358
|
obsoleteRuns,
|
|
6251
|
-
prunedRuns,
|
|
6359
|
+
prunedRuns: [...prunedOrphanedRuns, ...prunedRuns],
|
|
6252
6360
|
markedObsoleteRun,
|
|
6253
6361
|
selectedRun,
|
|
6254
6362
|
runCount: journals.length
|