@treeseed/sdk 0.12.59 → 0.12.61
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.js +59 -56
- package/dist/hosting/contracts.d.ts +0 -19
- package/dist/hosting/graph.d.ts +1 -86
- package/dist/hosting/graph.js +96 -247
- package/dist/local-dev/managed-dev.js +30 -8
- package/dist/managed-dependencies.d.ts +3 -0
- package/dist/managed-dependencies.js +294 -20
- package/dist/operations/services/deploy.js +6 -6
- package/dist/operations/services/deployment-readiness.js +5 -4
- package/dist/operations/services/git-runner.d.ts +2 -0
- package/dist/operations/services/git-runner.js +23 -2
- package/dist/operations/services/hosted-service-checks.js +28 -0
- package/dist/operations/services/live-hosted-service-checks.js +56 -14
- package/dist/operations/services/local-cleanup.d.ts +1 -0
- package/dist/operations/services/local-cleanup.js +28 -9
- package/dist/operations/services/package-adapters.js +3 -3
- package/dist/operations/services/railway-api.d.ts +72 -28
- package/dist/operations/services/railway-api.js +321 -876
- package/dist/operations/services/railway-cli.d.ts +47 -0
- package/dist/operations/services/railway-cli.js +142 -0
- package/dist/operations/services/railway-deploy.d.ts +5 -0
- package/dist/operations/services/railway-deploy.js +47 -75
- package/dist/operations/services/railway-source-policy.d.ts +6 -0
- package/dist/operations/services/railway-source-policy.js +52 -9
- package/dist/operations/services/repository-save-orchestrator.d.ts +2 -0
- package/dist/operations/services/repository-save-orchestrator.js +45 -14
- package/dist/operations-types.d.ts +3 -1
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +2 -1
- package/dist/reconcile/builtin-adapters.js +524 -680
- package/dist/reconcile/desired-state.js +5 -3
- package/dist/reconcile/engine.js +34 -28
- package/dist/reconcile/live-acceptance.js +2 -11
- package/dist/reconcile/providers/railway-iac.d.ts +148 -0
- package/dist/reconcile/providers/railway-iac.js +294 -18
- package/dist/scenes/runner.js +11 -11
- package/dist/scripts/build-dist.js +22 -0
- package/dist/workflow/operations.d.ts +12 -0
- package/dist/workflow/operations.js +265 -90
- package/dist/workflow/runs.d.ts +4 -0
- package/dist/workflow/runs.js +25 -0
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +3 -1
- package/package.json +1 -2
|
@@ -153,7 +153,6 @@ import {
|
|
|
153
153
|
archiveWorkflowRun,
|
|
154
154
|
cacheWorkflowGateResult,
|
|
155
155
|
classifyWorkflowRunJournal,
|
|
156
|
-
classifyWorkflowRunJournals,
|
|
157
156
|
createWorkflowRunJournal,
|
|
158
157
|
generateWorkflowRunId,
|
|
159
158
|
getCachedSuccessfulWorkflowGate,
|
|
@@ -551,11 +550,12 @@ function selectorFromWorkflowHostingGraph(graph) {
|
|
|
551
550
|
].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
|
|
552
551
|
serviceId: [...new Set(graph.units.flatMap((unit) => [
|
|
553
552
|
unit.id,
|
|
553
|
+
typeof unit.config.poolKey === "string" ? unit.config.poolKey : null,
|
|
554
554
|
typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
|
|
555
555
|
]).concat(domainServiceIds).filter((value) => Boolean(value)))],
|
|
556
556
|
serviceType: [...new Set(graph.units.flatMap((unit) => {
|
|
557
557
|
if (unit.id === "api") return ["api-runtime", "railway-service:api", "custom-domain:api", "dns-record"];
|
|
558
|
-
if (unit.id === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
|
|
558
|
+
if (unit.id === "operationsRunner" || unit.config.poolKey === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
|
|
559
559
|
if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
|
|
560
560
|
if (unit.host.id === "cloudflare") return ["web-ui", "edge-worker", "content-store", "database", "kv-form-guard", "turnstile-widget", "pages-project", "custom-domain:web", "dns-record"];
|
|
561
561
|
return [];
|
|
@@ -707,6 +707,40 @@ ${liveFailures.join("\n")}`, {
|
|
|
707
707
|
}
|
|
708
708
|
return live;
|
|
709
709
|
}
|
|
710
|
+
async function verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs) {
|
|
711
|
+
const reports = {};
|
|
712
|
+
for (const environment of ["prod", "staging"]) {
|
|
713
|
+
const env = {
|
|
714
|
+
...helpers.context.env,
|
|
715
|
+
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env),
|
|
716
|
+
...environment === "prod" ? releaseImageRefs : {}
|
|
717
|
+
};
|
|
718
|
+
helpers.write(`[release][railway] read-only ${environment} source-invariance verification started.`, "stderr");
|
|
719
|
+
const report = await collectTreeseedLiveHostedServiceChecks({
|
|
720
|
+
tenantRoot: root,
|
|
721
|
+
target: environment,
|
|
722
|
+
appId: "api",
|
|
723
|
+
serviceKeys: ["api", "operationsRunner", "public-treedx-node-01"],
|
|
724
|
+
strict: true,
|
|
725
|
+
requireLiveRailway: true,
|
|
726
|
+
requireLiveHttp: true,
|
|
727
|
+
env
|
|
728
|
+
});
|
|
729
|
+
const failures = [
|
|
730
|
+
...report.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
|
|
731
|
+
...report.liveObservation.issues
|
|
732
|
+
];
|
|
733
|
+
if (failures.length > 0) {
|
|
734
|
+
workflowError("release", "hosted_live_verification_failed", `${environment} API source-invariance verification failed after production deployment:
|
|
735
|
+
${failures.join("\n")}`, {
|
|
736
|
+
details: { environment, report }
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
reports[environment] = report;
|
|
740
|
+
helpers.write(`[release][railway] read-only ${environment} source-invariance verification passed.`, "stderr");
|
|
741
|
+
}
|
|
742
|
+
return { status: "verified", order: ["prod", "staging"], reports };
|
|
743
|
+
}
|
|
710
744
|
function productionReleaseImageRefEnv(selectedVersions) {
|
|
711
745
|
const refs = {};
|
|
712
746
|
const apiVersion = selectedVersions.get("@treeseed/api");
|
|
@@ -1797,7 +1831,32 @@ function gateForSavedRootReport(report, branch, scope) {
|
|
|
1797
1831
|
}
|
|
1798
1832
|
function findAutoResumableTaskRun(root, command, branch) {
|
|
1799
1833
|
if (!branch) return null;
|
|
1800
|
-
|
|
1834
|
+
const currentHeads = Object.fromEntries([
|
|
1835
|
+
["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
|
|
1836
|
+
...checkedOutWorkspacePackageRepos(root).map((repo) => [
|
|
1837
|
+
repo.name,
|
|
1838
|
+
runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
|
|
1839
|
+
])
|
|
1840
|
+
]);
|
|
1841
|
+
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
1842
|
+
if (journal.command !== command || !journal.resumable || journal.session.branchName !== branch) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1846
|
+
currentBranch: branch,
|
|
1847
|
+
currentHeads
|
|
1848
|
+
});
|
|
1849
|
+
if (classification.state === "resumable") {
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
if (classification.state === "stale") {
|
|
1853
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1854
|
+
...classification,
|
|
1855
|
+
reasons: [`${command} implicit resume skipped stale failed run`, ...classification.reasons]
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
return false;
|
|
1859
|
+
}) ?? null;
|
|
1801
1860
|
}
|
|
1802
1861
|
function rejectImplicitWorkflowResume(operation, journal) {
|
|
1803
1862
|
if (!journal) return;
|
|
@@ -2011,17 +2070,48 @@ async function executeJournalStep(root, runId, stepId, action, options = {}) {
|
|
|
2011
2070
|
if (step.status === "completed" && !options.rerunCompleted) {
|
|
2012
2071
|
return step.data ?? null;
|
|
2013
2072
|
}
|
|
2014
|
-
const
|
|
2073
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
2074
|
+
const retryCount = Number(step.retryCount ?? 0) + (step.startedAt ? 1 : 0);
|
|
2075
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2076
|
+
...journal,
|
|
2077
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, startedAt: startedAt.toISOString(), retryCount, lastFailure: null } : entry)
|
|
2078
|
+
}));
|
|
2079
|
+
refreshWorkflowLock(root, runId);
|
|
2080
|
+
const lockHeartbeat = setInterval(() => refreshWorkflowLock(root, runId), 15e3);
|
|
2081
|
+
lockHeartbeat.unref();
|
|
2082
|
+
process.stderr.write(`[workflow][step] start ${stepId} attempt=${retryCount + 1}
|
|
2083
|
+
`);
|
|
2084
|
+
let data;
|
|
2085
|
+
try {
|
|
2086
|
+
data = await Promise.resolve(action());
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
clearInterval(lockHeartbeat);
|
|
2089
|
+
const elapsedMs2 = Date.now() - startedAt.getTime();
|
|
2090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2091
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2092
|
+
...journal,
|
|
2093
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, elapsedMs: elapsedMs2, lastFailure: message } : entry)
|
|
2094
|
+
}));
|
|
2095
|
+
process.stderr.write(`[workflow][step] fail ${stepId} elapsed=${Math.ceil(elapsedMs2 / 1e3)}s retries=${retryCount}
|
|
2096
|
+
`);
|
|
2097
|
+
throw error;
|
|
2098
|
+
}
|
|
2099
|
+
clearInterval(lockHeartbeat);
|
|
2100
|
+
const elapsedMs = Date.now() - startedAt.getTime();
|
|
2015
2101
|
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2016
2102
|
...journal,
|
|
2017
2103
|
steps: journal.steps.map((entry) => entry.id === stepId ? {
|
|
2018
2104
|
...entry,
|
|
2019
2105
|
status: "completed",
|
|
2020
2106
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2107
|
+
elapsedMs,
|
|
2108
|
+
lastFailure: null,
|
|
2021
2109
|
data: data ?? null
|
|
2022
2110
|
} : entry)
|
|
2023
2111
|
}));
|
|
2024
2112
|
refreshWorkflowLock(root, runId);
|
|
2113
|
+
process.stderr.write(`[workflow][step] complete ${stepId} elapsed=${Math.ceil(elapsedMs / 1e3)}s retries=${retryCount}
|
|
2114
|
+
`);
|
|
2025
2115
|
return data;
|
|
2026
2116
|
}
|
|
2027
2117
|
function skipJournalStep(root, runId, stepId, data = null) {
|
|
@@ -2185,7 +2275,7 @@ function productionPackageDeployGates(root, versions) {
|
|
|
2185
2275
|
})];
|
|
2186
2276
|
});
|
|
2187
2277
|
}
|
|
2188
|
-
function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
2278
|
+
async function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
2189
2279
|
const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
|
|
2190
2280
|
if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
|
|
2191
2281
|
const tsx = resolve(root, "node_modules/.bin/tsx");
|
|
@@ -2199,7 +2289,7 @@ function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
|
2199
2289
|
return {
|
|
2200
2290
|
status: "npm-install",
|
|
2201
2291
|
adapter: adapter?.id ?? pkg.name,
|
|
2202
|
-
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
2292
|
+
...await runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
2203
2293
|
};
|
|
2204
2294
|
}
|
|
2205
2295
|
return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
|
|
@@ -2237,40 +2327,83 @@ function npmCommandForWorkflowSpawn(args) {
|
|
|
2237
2327
|
]
|
|
2238
2328
|
};
|
|
2239
2329
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2330
|
+
function lockfileRootMatchesManifest(repoDir) {
|
|
2331
|
+
try {
|
|
2332
|
+
const manifest = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
|
|
2333
|
+
const lockfile = JSON.parse(readFileSync(resolve(repoDir, "package-lock.json"), "utf8"));
|
|
2334
|
+
const root = lockfile.packages?.[""];
|
|
2335
|
+
if (!root || root.version !== manifest.version) return false;
|
|
2336
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
2337
|
+
const expected = manifest[field] ?? {};
|
|
2338
|
+
const observed = root[field] ?? {};
|
|
2339
|
+
if (JSON.stringify(expected) !== JSON.stringify(observed)) return false;
|
|
2340
|
+
}
|
|
2341
|
+
return true;
|
|
2342
|
+
} catch {
|
|
2343
|
+
return false;
|
|
2243
2344
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2345
|
+
}
|
|
2346
|
+
function waitForReleaseInstall(command, args, repoDir, attempt) {
|
|
2347
|
+
return new Promise((resolvePromise) => {
|
|
2348
|
+
const startedAt = Date.now();
|
|
2349
|
+
let settled = false;
|
|
2350
|
+
const child = spawn(command, args, {
|
|
2249
2351
|
cwd: repoDir,
|
|
2250
2352
|
env: {
|
|
2251
2353
|
...process.env,
|
|
2252
2354
|
npm_config_audit: "false",
|
|
2253
|
-
npm_config_fetch_retries: "
|
|
2355
|
+
npm_config_fetch_retries: "2",
|
|
2254
2356
|
npm_config_fund: "false",
|
|
2255
|
-
npm_config_foreground_scripts: "
|
|
2357
|
+
npm_config_foreground_scripts: "false",
|
|
2256
2358
|
npm_config_loglevel: "warn",
|
|
2257
2359
|
npm_config_maxsockets: "4",
|
|
2258
|
-
|
|
2360
|
+
npm_config_prefer_offline: "true",
|
|
2259
2361
|
npm_config_progress: "false"
|
|
2260
2362
|
},
|
|
2261
|
-
stdio: "pipe",
|
|
2262
|
-
|
|
2363
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2364
|
+
});
|
|
2365
|
+
const output = [];
|
|
2366
|
+
child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
2367
|
+
child.stderr?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
2368
|
+
const heartbeat = setInterval(() => {
|
|
2369
|
+
process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
|
|
2370
|
+
`);
|
|
2371
|
+
}, 15e3);
|
|
2372
|
+
child.once("close", (status) => {
|
|
2373
|
+
if (settled) return;
|
|
2374
|
+
settled = true;
|
|
2375
|
+
clearInterval(heartbeat);
|
|
2376
|
+
resolvePromise({ status, detail: Buffer.concat(output).toString("utf8").trim() });
|
|
2377
|
+
});
|
|
2378
|
+
child.once("error", (error) => {
|
|
2379
|
+
if (settled) return;
|
|
2380
|
+
settled = true;
|
|
2381
|
+
clearInterval(heartbeat);
|
|
2382
|
+
output.push(Buffer.from(error.message));
|
|
2383
|
+
resolvePromise({ status: null, detail: Buffer.concat(output).toString("utf8").trim() });
|
|
2263
2384
|
});
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
async function runReleaseNpmInstall(repoDir, options = {}) {
|
|
2388
|
+
if (shouldSkipReleaseInstall()) {
|
|
2389
|
+
return { status: "skipped", reason: "disabled" };
|
|
2390
|
+
}
|
|
2391
|
+
if (repoDir === options.workspaceRoot && lockfileRootMatchesManifest(repoDir)) {
|
|
2392
|
+
return { status: "skipped", reason: "root-lockfile-already-matches", attempts: 0 };
|
|
2393
|
+
}
|
|
2394
|
+
const args = ["install", "--package-lock-only", "--ignore-scripts", "--prefer-offline", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2395
|
+
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
2396
|
+
let lastDetail = "";
|
|
2397
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
2398
|
+
process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=0s
|
|
2399
|
+
`);
|
|
2400
|
+
const result = await waitForReleaseInstall(spawnCommand.command, spawnCommand.args, repoDir, attempt);
|
|
2264
2401
|
if (result.status === 0) {
|
|
2265
2402
|
return { status: "completed", reason: null, attempts: attempt };
|
|
2266
2403
|
}
|
|
2267
|
-
lastDetail =
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
result.stdout?.trim()
|
|
2271
|
-
].filter(Boolean).join("\n");
|
|
2272
|
-
if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 10) break;
|
|
2273
|
-
spawnSync("sleep", ["30"], { stdio: "ignore" });
|
|
2404
|
+
lastDetail = result.detail;
|
|
2405
|
+
if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 3) break;
|
|
2406
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 15e3));
|
|
2274
2407
|
}
|
|
2275
2408
|
throw new Error(lastDetail || `npm ${args.join(" ")} failed`);
|
|
2276
2409
|
}
|
|
@@ -5192,6 +5325,7 @@ async function workflowClose(helpers, input) {
|
|
|
5192
5325
|
}
|
|
5193
5326
|
function stagingCandidateWorkflowGates(root, manifest) {
|
|
5194
5327
|
const gates = [];
|
|
5328
|
+
const adapters = discoverTreeseedPackageAdapters(root);
|
|
5195
5329
|
const add = (name, repoPath, headSha, workflow, deploy = false) => {
|
|
5196
5330
|
if (!workflowFileExists(repoPath, workflow)) return;
|
|
5197
5331
|
const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
|
|
@@ -5199,10 +5333,11 @@ function stagingCandidateWorkflowGates(root, manifest) {
|
|
|
5199
5333
|
};
|
|
5200
5334
|
for (const pkg of manifest.packages) {
|
|
5201
5335
|
const repoPath = resolve(root, pkg.path);
|
|
5336
|
+
const adapter = adapters.find((candidate) => candidate.id === pkg.name || candidate.name === pkg.name);
|
|
5202
5337
|
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
|
|
5203
5338
|
add(pkg.name, repoPath, pkg.commit, "verify.yml");
|
|
5204
5339
|
}
|
|
5205
|
-
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5340
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && adapter?.capabilities.deploy === true && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5206
5341
|
add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
|
|
5207
5342
|
}
|
|
5208
5343
|
}
|
|
@@ -6018,6 +6153,7 @@ ${blockers.join("\n")}`, {
|
|
|
6018
6153
|
},
|
|
6019
6154
|
{ id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6020
6155
|
{ 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 },
|
|
6156
|
+
{ id: "verify-api-environment-isolation", description: "Verify production images and staging Git sources remained isolated", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6021
6157
|
{ id: "persist-production-image-refs", description: "Persist released production image refs and verify deployment readiness", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6022
6158
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
6023
6159
|
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
@@ -6083,57 +6219,90 @@ ${rendered}`);
|
|
|
6083
6219
|
});
|
|
6084
6220
|
const packageReleases = [];
|
|
6085
6221
|
const packageRepoByName = new Map(checkedOutWorkspacePackageRepos(root).map((entry) => [entry.name, entry]));
|
|
6086
|
-
|
|
6222
|
+
const pendingPackageReleases = new Set(selectedPackageNames.filter((name) => selectedVersions.has(name) && packageRepoByName.has(name)));
|
|
6223
|
+
const completedPackageReleases = /* @__PURE__ */ new Set();
|
|
6224
|
+
const packageReleaseResults = /* @__PURE__ */ new Map();
|
|
6225
|
+
const selectedPackageDependencies = new Map([...pendingPackageReleases].map((packageName) => {
|
|
6087
6226
|
const pkg = packageRepoByName.get(packageName);
|
|
6088
|
-
if (!pkg)
|
|
6089
|
-
const
|
|
6090
|
-
if (!
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6227
|
+
if (!pkg) return [packageName, []];
|
|
6228
|
+
const manifest = readJsonFile(resolve(pkg.dir, "package.json"));
|
|
6229
|
+
if (!manifest) {
|
|
6230
|
+
throw new Error(`Release package manifest is missing or invalid for ${packageName}.`);
|
|
6231
|
+
}
|
|
6232
|
+
const dependencyNames = /* @__PURE__ */ new Set([
|
|
6233
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
6234
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
6235
|
+
...Object.keys(manifest.peerDependencies ?? {})
|
|
6236
|
+
]);
|
|
6237
|
+
const dependencies = [...dependencyNames].filter((name) => pendingPackageReleases.has(name));
|
|
6238
|
+
if (packageName !== "@treeseed/sdk" && pendingPackageReleases.has("@treeseed/sdk") && !dependencies.includes("@treeseed/sdk")) {
|
|
6239
|
+
dependencies.push("@treeseed/sdk");
|
|
6240
|
+
}
|
|
6241
|
+
return [packageName, dependencies];
|
|
6242
|
+
}));
|
|
6243
|
+
while (pendingPackageReleases.size > 0) {
|
|
6244
|
+
const eligible = selectedPackageNames.filter((packageName) => pendingPackageReleases.has(packageName) && (selectedPackageDependencies.get(packageName) ?? []).every((dependency) => completedPackageReleases.has(dependency)));
|
|
6245
|
+
if (eligible.length === 0) {
|
|
6246
|
+
throw new Error(`Release package dependency graph is cyclic or unresolved: ${[...pendingPackageReleases].join(", ")}.`);
|
|
6247
|
+
}
|
|
6248
|
+
const batch = eligible.slice(0, 2);
|
|
6249
|
+
helpers.write(`[release][packages] starting batch ${batch.join(", ")} (concurrency=${batch.length}/2).`, "stderr");
|
|
6250
|
+
const results = await Promise.all(batch.map(async (packageName) => {
|
|
6251
|
+
const pkg = packageRepoByName.get(packageName);
|
|
6252
|
+
const version = selectedVersions.get(pkg.name);
|
|
6253
|
+
const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
|
|
6254
|
+
const metadata = await prepareAdapterReleaseMetadata(root, pkg, version);
|
|
6255
|
+
const changelog = updateReleaseChangelog(pkg.dir, {
|
|
6256
|
+
version,
|
|
6257
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
6258
|
+
targetRef: "HEAD"
|
|
6259
|
+
});
|
|
6260
|
+
const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
|
|
6261
|
+
subject: `release: ${pkg.name} ${version}`,
|
|
6262
|
+
version,
|
|
6263
|
+
tagName: version,
|
|
6264
|
+
sourceRef: STAGING_BRANCH,
|
|
6265
|
+
targetRef: PRODUCTION_BRANCH,
|
|
6266
|
+
changelog
|
|
6267
|
+
}));
|
|
6268
|
+
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
6269
|
+
const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
|
|
6270
|
+
const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
|
|
6271
|
+
const publishGate = {
|
|
6272
|
+
name: pkg.name,
|
|
6273
|
+
repoPath: pkg.dir,
|
|
6274
|
+
workflow: releaseWorkflowForPackage(root, pkg.name),
|
|
6275
|
+
branch: version,
|
|
6276
|
+
headSha: commit.commitSha
|
|
6277
|
+
};
|
|
6278
|
+
const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
|
|
6279
|
+
root,
|
|
6280
|
+
runId: workflowRun.runId,
|
|
6281
|
+
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6282
|
+
});
|
|
6283
|
+
const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
|
|
6284
|
+
return {
|
|
6285
|
+
name: pkg.name,
|
|
6286
|
+
path: relative(root, pkg.dir),
|
|
6287
|
+
version,
|
|
6288
|
+
changelog,
|
|
6289
|
+
metadata,
|
|
6290
|
+
commit,
|
|
6291
|
+
promotion,
|
|
6292
|
+
tag,
|
|
6293
|
+
publishWait: publishWait2,
|
|
6294
|
+
publishedArtifacts: publishedArtifacts2
|
|
6295
|
+
};
|
|
6120
6296
|
});
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
commit,
|
|
6129
|
-
promotion,
|
|
6130
|
-
tag,
|
|
6131
|
-
publishWait: publishWait2,
|
|
6132
|
-
publishedArtifacts: publishedArtifacts2
|
|
6133
|
-
};
|
|
6134
|
-
});
|
|
6135
|
-
packageReleases.push(packageRelease);
|
|
6297
|
+
return [packageName, packageRelease];
|
|
6298
|
+
}));
|
|
6299
|
+
for (const [packageName, packageRelease] of results) {
|
|
6300
|
+
pendingPackageReleases.delete(packageName);
|
|
6301
|
+
completedPackageReleases.add(packageName);
|
|
6302
|
+
packageReleaseResults.set(packageName, packageRelease);
|
|
6303
|
+
}
|
|
6136
6304
|
}
|
|
6305
|
+
packageReleases.push(...selectedPackageNames.map((name) => packageReleaseResults.get(name)).filter((entry) => Boolean(entry)));
|
|
6137
6306
|
const managedHelperReleases = await executeJournalStep(root, workflowRun.runId, "release-helper-repos", () => {
|
|
6138
6307
|
const releases = releaseHelperRepos.map((repo) => releaseHelperRepoToProduction(repo));
|
|
6139
6308
|
syncAllCheckedOutReleaseHelperRepos(root, STAGING_BRANCH);
|
|
@@ -6151,6 +6320,10 @@ ${rendered}`);
|
|
|
6151
6320
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6152
6321
|
}).then((workflowGates) => ({ workflowGates }));
|
|
6153
6322
|
});
|
|
6323
|
+
const apiEnvironmentIsolation = effectiveInput.verifyDeployedResources === true ? await executeJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", () => verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs)) : (skipJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", {
|
|
6324
|
+
status: "skipped",
|
|
6325
|
+
reason: "--verify-deployed-resources was not requested"
|
|
6326
|
+
}), { status: "skipped", reason: "--verify-deployed-resources was not requested" });
|
|
6154
6327
|
const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
|
|
6155
6328
|
const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
|
|
6156
6329
|
if (helpers.context.env) {
|
|
@@ -6171,8 +6344,8 @@ ${failures.join("\n")}`, {
|
|
|
6171
6344
|
}
|
|
6172
6345
|
return { persisted, readiness };
|
|
6173
6346
|
});
|
|
6174
|
-
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
6175
|
-
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6347
|
+
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", async () => {
|
|
6348
|
+
const rootInstall = await runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6176
6349
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
6177
6350
|
version: plannedRelease.rootVersion,
|
|
6178
6351
|
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
@@ -6244,6 +6417,7 @@ ${failures.join("\n")}`, {
|
|
|
6244
6417
|
publishWait: publishWait.workflowGates,
|
|
6245
6418
|
publishedArtifacts,
|
|
6246
6419
|
productionPackageDeployWorkflows,
|
|
6420
|
+
apiEnvironmentIsolation,
|
|
6247
6421
|
productionImageRefs,
|
|
6248
6422
|
backMerge,
|
|
6249
6423
|
workspaceLinks,
|
|
@@ -6397,17 +6571,21 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6397
6571
|
return null;
|
|
6398
6572
|
}).filter((entry) => entry !== null);
|
|
6399
6573
|
const journals = listWorkflowRunJournals(root);
|
|
6574
|
+
const actionableJournals = journals.filter((journal) => journal.status !== "completed" && !journal.classification?.archivedAt);
|
|
6400
6575
|
const session = resolveTreeseedWorkflowSession(root);
|
|
6401
6576
|
const currentHeads = Object.fromEntries(
|
|
6402
6577
|
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
6403
6578
|
);
|
|
6404
|
-
const classifiedRuns =
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6579
|
+
const classifiedRuns = actionableJournals.map((journal) => ({
|
|
6580
|
+
journal,
|
|
6581
|
+
classification: classifyWorkflowRunJournal(journal, {
|
|
6582
|
+
currentBranch: session.branchName,
|
|
6583
|
+
currentHeads
|
|
6584
|
+
})
|
|
6585
|
+
}));
|
|
6408
6586
|
const markedObsoleteRun = input.obsoleteRunId ? (() => {
|
|
6409
|
-
const
|
|
6410
|
-
if (!
|
|
6587
|
+
const journal = journals.find((candidate) => candidate.runId === input.obsoleteRunId);
|
|
6588
|
+
if (!journal) {
|
|
6411
6589
|
workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
|
|
6412
6590
|
}
|
|
6413
6591
|
const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
|
|
@@ -6416,17 +6594,14 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6416
6594
|
reasons: [reason],
|
|
6417
6595
|
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6418
6596
|
};
|
|
6419
|
-
archiveWorkflowRun(root,
|
|
6597
|
+
archiveWorkflowRun(root, journal.runId, classification);
|
|
6420
6598
|
return {
|
|
6421
|
-
runId:
|
|
6422
|
-
command:
|
|
6599
|
+
runId: journal.runId,
|
|
6600
|
+
command: journal.command,
|
|
6423
6601
|
reason
|
|
6424
6602
|
};
|
|
6425
6603
|
})() : null;
|
|
6426
|
-
const effectiveClassifiedRuns = markedObsoleteRun ?
|
|
6427
|
-
currentBranch: session.branchName,
|
|
6428
|
-
currentHeads
|
|
6429
|
-
}) : classifiedRuns;
|
|
6604
|
+
const effectiveClassifiedRuns = markedObsoleteRun ? classifiedRuns.filter((entry) => entry.journal.runId !== markedObsoleteRun.runId) : classifiedRuns;
|
|
6430
6605
|
const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
6431
6606
|
runId: journal.runId,
|
|
6432
6607
|
command: journal.command,
|
package/dist/workflow/runs.d.ts
CHANGED
|
@@ -30,7 +30,11 @@ export type TreeseedWorkflowRunStep = {
|
|
|
30
30
|
branch: string | null;
|
|
31
31
|
resumable: boolean;
|
|
32
32
|
status: 'pending' | 'completed' | 'skipped';
|
|
33
|
+
startedAt?: string | null;
|
|
33
34
|
completedAt: string | null;
|
|
35
|
+
elapsedMs?: number | null;
|
|
36
|
+
retryCount?: number;
|
|
37
|
+
lastFailure?: string | null;
|
|
34
38
|
data: Record<string, unknown> | null;
|
|
35
39
|
};
|
|
36
40
|
export type TreeseedWorkflowRunJournal = {
|
package/dist/workflow/runs.js
CHANGED
|
@@ -435,6 +435,18 @@ function collectSaveExpectedHeads(journal) {
|
|
|
435
435
|
}
|
|
436
436
|
return heads;
|
|
437
437
|
}
|
|
438
|
+
function collectStageExpectedHeads(journal) {
|
|
439
|
+
const heads = {};
|
|
440
|
+
const promotion = stringRecord(journal.steps.find((step) => step.id === "promote-to-staging")?.data);
|
|
441
|
+
const results = Array.isArray(promotion?.results) ? promotion.results : [];
|
|
442
|
+
for (const entry of results) {
|
|
443
|
+
const repo = stringRecord(entry);
|
|
444
|
+
if (typeof repo?.name === "string" && typeof repo.commitSha === "string" && repo.verified === true) {
|
|
445
|
+
heads[repo.name] = repo.commitSha;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return heads;
|
|
449
|
+
}
|
|
438
450
|
function classifyWorkflowRunJournal(journal, options = {}) {
|
|
439
451
|
const reasons = [];
|
|
440
452
|
const now = options.now ?? nowIso();
|
|
@@ -516,6 +528,15 @@ function classifyWorkflowRunJournal(journal, options = {}) {
|
|
|
516
528
|
}
|
|
517
529
|
}
|
|
518
530
|
}
|
|
531
|
+
if (journal.command === "stage" && options.currentHeads) {
|
|
532
|
+
const expectedHeads = collectStageExpectedHeads(journal);
|
|
533
|
+
for (const [name, expectedHead] of Object.entries(expectedHeads)) {
|
|
534
|
+
const currentHead = options.currentHeads[name];
|
|
535
|
+
if (currentHead && currentHead !== expectedHead) {
|
|
536
|
+
reasons.push(`${name} head changed from staged candidate ${expectedHead} to ${currentHead}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
519
540
|
return {
|
|
520
541
|
state: reasons.length > 0 ? "stale" : "resumable",
|
|
521
542
|
reasons: reasons.length > 0 ? reasons : releaseGateOnlyCompletion ? ["release commits already exist; remaining release gates can be rechecked"] : ["workflow run can be resumed"],
|
|
@@ -598,7 +619,11 @@ function createWorkflowRunJournal(root, options) {
|
|
|
598
619
|
steps: options.steps.map((step) => ({
|
|
599
620
|
...step,
|
|
600
621
|
status: "pending",
|
|
622
|
+
startedAt: null,
|
|
601
623
|
completedAt: null,
|
|
624
|
+
elapsedMs: null,
|
|
625
|
+
retryCount: 0,
|
|
626
|
+
lastFailure: null,
|
|
602
627
|
data: null
|
|
603
628
|
})),
|
|
604
629
|
failure: null,
|
|
@@ -10,7 +10,7 @@ export { readTreeseedVerificationCache, treeseedVerificationCacheKey, writeTrees
|
|
|
10
10
|
export { assertDeploymentInitialized, cleanupDestroyedState, createBranchPreviewDeployTarget, createPersistentDeployTarget, deployTargetLabel, destroyCloudflareResources, ensureGeneratedWranglerConfig, finalizeDeploymentState, loadDeployState, printDeploySummary, printDestroySummary, provisionCloudflareResources, runRemoteD1Migrations, syncCloudflareSecrets, validateDeployPrerequisites, validateDestroyPrerequisites, } from './operations/services/deploy.js';
|
|
11
11
|
export { assertCleanWorktree, assertFeatureBranch, branchExists, checkoutBranch, createFeatureBranchFromStaging, currentManagedBranch, deleteLocalBranch, deleteRemoteBranch, ensureLocalBranchTracking, gitWorkflowRoot, listTaskBranches, mergeCurrentBranchIntoStaging, mergeStagingIntoMain, prepareReleaseBranches, PRODUCTION_BRANCH, pushBranch, remoteBranchExists, STAGING_BRANCH, syncBranchWithOrigin, waitForStagingAutomation, } from './operations/services/git-workflow.js';
|
|
12
12
|
export { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, } from './operations/services/runtime-tools.js';
|
|
13
|
-
export { configuredRailwayServices, validateRailwayDeployPrerequisites, } from './operations/services/railway-deploy.js';
|
|
13
|
+
export { configuredRailwayServices, validateRailwayDeployPrerequisites, waitForRailwayManagedDeploymentsSettled, } from './operations/services/railway-deploy.js';
|
|
14
14
|
export { getRailwayAuthProfile, listRailwayEnvironments, listRailwayProjects, listRailwayServices, listRailwayVariables, resolveRailwayApiToken, resolveRailwayApiUrl, resolveRailwayWorkspace, resolveRailwayWorkspaceContext, } from './operations/services/railway-api.js';
|
|
15
15
|
export { githubRepositoryCredentialEnvName, resolveGitHubCredentialForRepository, type TreeseedGitHubCredentialResolution, } from './operations/services/github-credentials.js';
|
|
16
16
|
export { createGitHubApiClient, ensureGitHubActionsEnvironment, getLatestGitHubWorkflowRun, listGitHubEnvironmentSecretNames, listGitHubEnvironmentVariableNames, type GitHubWorkflowDispatchResult, type GitHubWorkflowRunSummary, } from './operations/services/github-api.js';
|
package/dist/workflow-support.js
CHANGED
|
@@ -113,7 +113,8 @@ import {
|
|
|
113
113
|
} from "./operations/services/runtime-tools.js";
|
|
114
114
|
import {
|
|
115
115
|
configuredRailwayServices,
|
|
116
|
-
validateRailwayDeployPrerequisites
|
|
116
|
+
validateRailwayDeployPrerequisites,
|
|
117
|
+
waitForRailwayManagedDeploymentsSettled
|
|
117
118
|
} from "./operations/services/railway-deploy.js";
|
|
118
119
|
import {
|
|
119
120
|
getRailwayAuthProfile,
|
|
@@ -379,6 +380,7 @@ export {
|
|
|
379
380
|
validateRailwayDeployPrerequisites,
|
|
380
381
|
validateTreeseedPackageManifests,
|
|
381
382
|
verifyTreeseedPackageArtifact,
|
|
383
|
+
waitForRailwayManagedDeploymentsSettled,
|
|
382
384
|
waitForStagingAutomation,
|
|
383
385
|
warnDeprecatedTreeseedLocalEnvFiles,
|
|
384
386
|
withTreeseedKeyAgentAutopromptDisabled,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/sdk",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.61",
|
|
4
4
|
"description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"@github/copilot": "1.0.39",
|
|
67
67
|
"@github/copilot-language-server": "1.480.0",
|
|
68
68
|
"@github/copilot-sdk": "0.3.0",
|
|
69
|
-
"@railway/cli": "5.23.2",
|
|
70
69
|
"@remotion/bundler": "4.0.477",
|
|
71
70
|
"@remotion/renderer": "4.0.477",
|
|
72
71
|
"drizzle-orm": "^0.45.2",
|