@treeseed/sdk 0.12.58 → 0.12.60
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/hosting/graph.js +4 -6
- package/dist/operations/services/deploy.js +1 -1
- package/dist/operations/services/deployment-readiness.js +4 -4
- package/dist/operations/services/live-hosted-service-checks.js +9 -3
- package/dist/operations/services/railway-deploy.d.ts +5 -0
- package/dist/operations/services/railway-deploy.js +29 -2
- package/dist/operations/services/railway-source-policy.js +2 -2
- package/dist/operations/services/repository-save-orchestrator.js +11 -8
- package/dist/reconcile/builtin-adapters.js +13 -4
- package/dist/reconcile/providers/railway-iac.d.ts +1 -0
- package/dist/reconcile/providers/railway-iac.js +5 -2
- package/dist/workflow/operations.d.ts +4 -0
- package/dist/workflow/operations.js +45 -5
- package/dist/workflow/runs.js +21 -0
- package/package.json +1 -1
package/dist/hosting/graph.js
CHANGED
|
@@ -83,7 +83,7 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
83
83
|
imageRef,
|
|
84
84
|
imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
|
|
85
85
|
};
|
|
86
|
-
const serviceName =
|
|
86
|
+
const serviceName = "public-treedx-node-01";
|
|
87
87
|
assertApiRailwaySourcePolicy(input.environment, { key: serviceName, serviceName, ...policy2 });
|
|
88
88
|
return policy2;
|
|
89
89
|
}
|
|
@@ -176,8 +176,7 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
|
|
|
176
176
|
const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
|
|
177
177
|
const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
|
|
178
178
|
const baseServiceName = service.railway?.serviceName ?? null;
|
|
179
|
-
const
|
|
180
|
-
const serviceName = typeof configuredEnvironmentServiceName === "string" && configuredEnvironmentServiceName.trim() ? configuredEnvironmentServiceName.trim() : input.environment === "prod" && ["api", "operationsRunner"].includes(serviceKey) && baseServiceName ? `${baseServiceName}-production` : baseServiceName;
|
|
179
|
+
const serviceName = baseServiceName;
|
|
181
180
|
const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
|
|
182
181
|
const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
|
|
183
182
|
const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
|
|
@@ -384,9 +383,8 @@ function buildProfileFromDeployConfig(input) {
|
|
|
384
383
|
const imageRefEnv = railwayImageRefEnvForService(serviceKey);
|
|
385
384
|
const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
|
|
386
385
|
const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
|
|
387
|
-
const environmentConfig = service.environments?.[input.environment];
|
|
388
386
|
const baseServiceName = service.railway?.serviceName ?? null;
|
|
389
|
-
const effectiveServiceName =
|
|
387
|
+
const effectiveServiceName = baseServiceName;
|
|
390
388
|
services.push({
|
|
391
389
|
id: serviceKey,
|
|
392
390
|
label: placement === "runner-capacity" ? "Runner Capacity" : serviceKey === "api" ? "API Runtime" : serviceKey,
|
|
@@ -472,7 +470,7 @@ function buildProfileFromDeployConfig(input) {
|
|
|
472
470
|
const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
|
|
473
471
|
const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
|
|
474
472
|
const nodeIndex = offset + 1;
|
|
475
|
-
const serviceName = indexedName(
|
|
473
|
+
const serviceName = indexedName("public-treedx-node", nodeIndex);
|
|
476
474
|
return {
|
|
477
475
|
id: serviceName,
|
|
478
476
|
label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
|
|
@@ -2584,7 +2584,7 @@ function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
|
|
|
2584
2584
|
services.push({
|
|
2585
2585
|
key: "treeseedDatabase",
|
|
2586
2586
|
projectName: deployConfig.services?.api?.railway?.projectName ?? identity.deploymentKey,
|
|
2587
|
-
serviceName:
|
|
2587
|
+
serviceName: baseName.replace(/-(staging|prod|production)$/u, ""),
|
|
2588
2588
|
railwayEnvironment: normalizeRailwayEnvironmentName(normalizedScope),
|
|
2589
2589
|
domain: null,
|
|
2590
2590
|
volumeMountPath: null,
|
|
@@ -143,8 +143,8 @@ function collectTreeseedDeploymentReadiness(options) {
|
|
|
143
143
|
checks.push(check("hosting:api:healthcheckPath", api.config?.healthcheckPath, "/healthz", "API healthcheck path is /healthz.", "Set services.api.railway.healthcheckPath to /healthz.", "healthcheckPath"));
|
|
144
144
|
}
|
|
145
145
|
if (railwayApi) {
|
|
146
|
-
const expectedServiceName =
|
|
147
|
-
checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, expectedServiceName, "Railway API service uses the
|
|
146
|
+
const expectedServiceName = "treeseed-api";
|
|
147
|
+
checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, expectedServiceName, "Railway API service uses the project-wide canonical name.", `Set the API service identity to ${expectedServiceName} for every environment.`, "serviceName"));
|
|
148
148
|
checks.push(check("railway-config:api:rootDirectory", relRoot(tenantRoot, railwayApi.rootDir), expectedApiRailwayRoot, "Railway API effective rootDirectory points at the API app.", "Set services.api.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
|
|
149
149
|
}
|
|
150
150
|
if (!runner) {
|
|
@@ -177,8 +177,8 @@ function collectTreeseedDeploymentReadiness(options) {
|
|
|
177
177
|
checks.push(check("hosting:operationsRunner:volumeMountPath", runner.config?.volumeMountPath, "/data", "Runner volume mount path is stable.", "Set services.operationsRunner.railway.volumeMountPath to /data.", "volumeMountPath"));
|
|
178
178
|
}
|
|
179
179
|
if (railwayRunner) {
|
|
180
|
-
const expectedServiceName =
|
|
181
|
-
checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, expectedServiceName, "Railway runner
|
|
180
|
+
const expectedServiceName = "treeseed-api-operations-runner-01";
|
|
181
|
+
checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, expectedServiceName, "Railway runner uses the project-wide canonical service name.", `Set the runner service identity to ${expectedServiceName} for every environment.`, "serviceName"));
|
|
182
182
|
checks.push(check("railway-config:operationsRunner:rootDirectory", relRoot(tenantRoot, railwayRunner.rootDir), expectedRunnerRailwayRoot, "Railway runner effective rootDirectory points at the API app.", "Set services.operationsRunner.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
|
|
183
183
|
}
|
|
184
184
|
if (!database) {
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
configuredRailwayServices,
|
|
17
17
|
findStaleTreeseedOperationsRunnerResources,
|
|
18
|
-
isTreeseedOperationsRunnerResourceName
|
|
18
|
+
isTreeseedOperationsRunnerResourceName,
|
|
19
|
+
railwayLegacyAliasMigrationPolicy
|
|
19
20
|
} from "./railway-deploy.js";
|
|
20
21
|
import { discoverTreeseedApplications } from "../../hosting/apps.js";
|
|
21
22
|
import {
|
|
@@ -241,6 +242,7 @@ async function collectRailwayObservations(options) {
|
|
|
241
242
|
const configuredServices = configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => serviceMatchesAppSelection(entry, options.tenantRoot, options.appId, applications)).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key));
|
|
242
243
|
const siblingTarget = options.target === "staging" ? "prod" : options.target === "prod" ? "staging" : null;
|
|
243
244
|
const configuredSiblingServices = siblingTarget ? configuredRailwayServices(options.tenantRoot, siblingTarget, options.env, { identityOnly: true }).filter((entry) => entry.enabled !== false).filter((entry) => entry.key === "operationsRunner") : [];
|
|
245
|
+
const retainedMigrationAliases = options.target === "staging" ? railwayLegacyAliasMigrationPolicy("staging", configuredServices).retainedResourceNames : [];
|
|
244
246
|
if (selectedServiceKeys.size === 0 || selectedServiceKeys.has("api") || selectedServiceKeys.has("operationsRunner")) {
|
|
245
247
|
for (const descriptor of treeseedDatabaseDescriptors(options.tenantRoot, options)) {
|
|
246
248
|
await verifyRailwayPostgresTopology({ descriptor, configuredServices, projects, options, issues });
|
|
@@ -286,14 +288,18 @@ async function collectRailwayObservations(options) {
|
|
|
286
288
|
inspectedRunnerScopes.add(runnerScope);
|
|
287
289
|
const desiredRunnerNames = /* @__PURE__ */ new Set([
|
|
288
290
|
...configuredServices.filter((entry) => entry.key === "operationsRunner").filter((entry) => entry.projectId ? entry.projectId === project.id : entry.projectName === project.name).filter((entry) => normalizeRailwayEnvironmentName(entry.railwayEnvironment) === normalizeRailwayEnvironmentName(environment.name)).map((entry) => entry.serviceName).filter(Boolean),
|
|
289
|
-
...configuredSiblingServices.filter((entry) => entry.projectId ? entry.projectId === project.id : entry.projectName === project.name).map((entry) => entry.serviceName).filter((name) => Boolean(name) && isTreeseedOperationsRunnerResourceName(name))
|
|
291
|
+
...configuredSiblingServices.filter((entry) => entry.projectId ? entry.projectId === project.id : entry.projectName === project.name).map((entry) => entry.serviceName).filter((name) => Boolean(name) && isTreeseedOperationsRunnerResourceName(name)),
|
|
292
|
+
...retainedMigrationAliases.filter((name) => isTreeseedOperationsRunnerResourceName(name) && !name.endsWith("-volume"))
|
|
290
293
|
]);
|
|
291
294
|
const desiredRunnerServiceIds = new Set(services.filter((entry) => desiredRunnerNames.has(entry.name)).map((entry) => entry.id));
|
|
292
295
|
for (const staleService of findStaleTreeseedOperationsRunnerResources(services, desiredRunnerNames)) {
|
|
293
296
|
issues.push(`${staleService.name}: stale operations runner Railway service remains in project ${project.name}.`);
|
|
294
297
|
}
|
|
295
298
|
const volumes2 = await listRailwayVolumes({ projectId: project.id, env: options.env, fetchImpl: options.fetchImpl }).catch(() => []);
|
|
296
|
-
const desiredRunnerVolumeNames = new Set([
|
|
299
|
+
const desiredRunnerVolumeNames = /* @__PURE__ */ new Set([
|
|
300
|
+
...[...desiredRunnerNames].map((name) => `${name}-volume`),
|
|
301
|
+
...retainedMigrationAliases.filter((name) => name.endsWith("-volume"))
|
|
302
|
+
]);
|
|
297
303
|
for (const staleVolume of findStaleTreeseedOperationsRunnerResources(volumes2, desiredRunnerVolumeNames)) {
|
|
298
304
|
const activeInstances = activeRailwayVolumeInstances(staleVolume);
|
|
299
305
|
const relevant = staleVolume.instances.length === 0 || activeInstances.length > 0;
|
|
@@ -60,6 +60,11 @@ export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any,
|
|
|
60
60
|
message: string;
|
|
61
61
|
}>;
|
|
62
62
|
export declare function configuredRailwayServices(tenantRoot: any, scope: any, envOverlay?: {}, options?: {}): unknown[];
|
|
63
|
+
export declare function legacyEnvironmentSpecificRailwayResourceNames(services: ReturnType<typeof configuredRailwayServices>): string[];
|
|
64
|
+
export declare function railwayLegacyAliasMigrationPolicy(scope: 'staging' | 'prod', services: ReturnType<typeof configuredRailwayServices>): {
|
|
65
|
+
retainedResourceNames: string[];
|
|
66
|
+
allowedResourceDeletions: string[];
|
|
67
|
+
};
|
|
63
68
|
export declare function configuredRailwayScheduledJobs(tenantRoot: any, scope: any, { phase }?: {
|
|
64
69
|
phase?: string | undefined;
|
|
65
70
|
}): any[];
|
|
@@ -686,7 +686,7 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
|
|
|
686
686
|
const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? (serviceKey === "api" ? configuredApiPublicBaseUrl(deployConfig, normalizedScope) : null);
|
|
687
687
|
const environmentConfig = service.environments?.[normalizedScope];
|
|
688
688
|
const baseServiceName = service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`);
|
|
689
|
-
const configuredServiceName =
|
|
689
|
+
const configuredServiceName = baseServiceName;
|
|
690
690
|
const configuredRunnerPool = service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
|
|
691
691
|
const runnerPool = serviceKey === "operationsRunner" || serviceKey === "capacityProviderRunner" ? {
|
|
692
692
|
bootstrapCount: Math.max(1, Number.parseInt(String(configuredRunnerPool?.bootstrapCount ?? (serviceKey === "capacityProviderRunner" ? 1 : OPERATIONS_RUNNER_BOOTSTRAP_COUNT)), 10) || (serviceKey === "capacityProviderRunner" ? 1 : OPERATIONS_RUNNER_BOOTSTRAP_COUNT)),
|
|
@@ -797,7 +797,7 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
|
|
|
797
797
|
const baseImageRef = envValue("TREESEED_PUBLIC_TREEDX_IMAGE_REF", imageRefEnv) || "treeseed/treedx";
|
|
798
798
|
return Array.from({ length: bootstrapCount }, (_, offset) => {
|
|
799
799
|
const index = offset + 1;
|
|
800
|
-
const serviceName =
|
|
800
|
+
const serviceName = `${PUBLIC_TREEDX_NODE_SERVICE_KEY_PREFIX}${String(index).padStart(2, "0")}`;
|
|
801
801
|
const service = {
|
|
802
802
|
key: serviceName,
|
|
803
803
|
serviceName,
|
|
@@ -985,6 +985,31 @@ function configuredRailwayServices(tenantRoot, scope, envOverlay = {}, options =
|
|
|
985
985
|
));
|
|
986
986
|
return [...direct, ...nested];
|
|
987
987
|
}
|
|
988
|
+
function legacyEnvironmentSpecificRailwayResourceNames(services) {
|
|
989
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
990
|
+
for (const service of services) {
|
|
991
|
+
let alias = null;
|
|
992
|
+
if (service.key === "api") {
|
|
993
|
+
alias = `${service.serviceName}-production`;
|
|
994
|
+
} else if (service.key === "operationsRunner") {
|
|
995
|
+
alias = service.serviceName.replace(/-(\d+)$/u, "-production-$1");
|
|
996
|
+
} else {
|
|
997
|
+
const treeDxMatch = service.serviceName.match(/^public-treedx-node-(\d+)$/u);
|
|
998
|
+
if (treeDxMatch?.[1]) alias = `public-treedx-node-production-${treeDxMatch[1]}`;
|
|
999
|
+
}
|
|
1000
|
+
if (!alias || alias === service.serviceName) continue;
|
|
1001
|
+
aliases.add(alias);
|
|
1002
|
+
if (service.volumeMountPath) aliases.add(`${alias}-volume`);
|
|
1003
|
+
}
|
|
1004
|
+
return [...aliases];
|
|
1005
|
+
}
|
|
1006
|
+
function railwayLegacyAliasMigrationPolicy(scope, services) {
|
|
1007
|
+
const aliases = legacyEnvironmentSpecificRailwayResourceNames(services);
|
|
1008
|
+
return {
|
|
1009
|
+
retainedResourceNames: scope === "staging" ? aliases : [],
|
|
1010
|
+
allowedResourceDeletions: scope === "prod" ? aliases : []
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
988
1013
|
function configuredRailwayScheduledJobs(tenantRoot, scope, { phase = "deploy" } = {}) {
|
|
989
1014
|
if (!shouldManageRailwaySchedules(scope, phase)) {
|
|
990
1015
|
return [];
|
|
@@ -1823,7 +1848,9 @@ export {
|
|
|
1823
1848
|
findStaleTreeseedOperationsRunnerResources,
|
|
1824
1849
|
isTreeseedOperationsRunnerResourceName,
|
|
1825
1850
|
isUsableRailwayToken,
|
|
1851
|
+
legacyEnvironmentSpecificRailwayResourceNames,
|
|
1826
1852
|
parseRailwayJsonOutput,
|
|
1853
|
+
railwayLegacyAliasMigrationPolicy,
|
|
1827
1854
|
railwayServiceRuntimeStartCommand,
|
|
1828
1855
|
resolveRailwayAuthToken,
|
|
1829
1856
|
resolveRailwayDeploymentProfile,
|
|
@@ -28,7 +28,7 @@ function assertApiRailwaySourcePolicy(scope, service) {
|
|
|
28
28
|
const label = service.serviceName ?? service.key ?? "Railway service";
|
|
29
29
|
if (normalizedScope === "staging") {
|
|
30
30
|
const issues = [
|
|
31
|
-
/-production(?:-|$)/u.test(String(service.serviceName ?? "")) ? "serviceName must use the
|
|
31
|
+
/-production(?:-|$)/u.test(String(service.serviceName ?? "")) ? "serviceName must use the project-wide canonical identity" : null,
|
|
32
32
|
service.sourceMode === "git" ? null : "sourceMode must be git",
|
|
33
33
|
service.imageRef ? "imageRef must be empty" : null,
|
|
34
34
|
service.sourceRepo ? null : "sourceRepo must be set",
|
|
@@ -43,7 +43,7 @@ function assertApiRailwaySourcePolicy(scope, service) {
|
|
|
43
43
|
}
|
|
44
44
|
if (normalizedScope === "prod") {
|
|
45
45
|
const issues = [
|
|
46
|
-
/-production(?:-|$)/u.test(String(service.serviceName ?? "")) ?
|
|
46
|
+
/-production(?:-|$)/u.test(String(service.serviceName ?? "")) ? "serviceName must use the project-wide canonical identity" : null,
|
|
47
47
|
service.sourceMode === "image" ? null : "sourceMode must be image",
|
|
48
48
|
isImmutableRailwayImageRef(service.imageRef) ? null : "imageRef must be an immutable released image tag",
|
|
49
49
|
service.sourceRepo ? "sourceRepo must be empty" : null,
|
|
@@ -944,9 +944,10 @@ async function runGitDependencySmoke(node, options, reference) {
|
|
|
944
944
|
void reference;
|
|
945
945
|
}
|
|
946
946
|
async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs = []) {
|
|
947
|
-
if (shouldSkipNetworkInstall()) {
|
|
948
|
-
|
|
949
|
-
|
|
947
|
+
if (shouldSkipNetworkInstall() || options.deferPushUntilVerified === true) {
|
|
948
|
+
const reason = options.deferPushUntilVerified === true ? "atomic-save" : "disabled";
|
|
949
|
+
emitProgress(options, node, "install", `Skipped npm install because ${reason === "atomic-save" ? "atomic save validates local lock metadata before publishing commits" : "network install mode is disabled"}.`);
|
|
950
|
+
return { status: "skipped", attempts: 0, reason };
|
|
950
951
|
}
|
|
951
952
|
let lastError = null;
|
|
952
953
|
const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
|
|
@@ -1030,9 +1031,10 @@ async function validateRepositoryLockfile(node, options) {
|
|
|
1030
1031
|
}
|
|
1031
1032
|
const { command, args } = lockfileValidationCommand(node, options);
|
|
1032
1033
|
const commandText = `${command} ${args.join(" ")}`;
|
|
1033
|
-
if (shouldSkipNetworkInstall()) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1034
|
+
if (shouldSkipNetworkInstall() || options.deferPushUntilVerified === true) {
|
|
1035
|
+
const reason = options.deferPushUntilVerified === true ? "atomic-save" : "disabled";
|
|
1036
|
+
emitProgress(options, node, "lockfile", `Validated lockfile structure without ${commandText} because ${reason === "atomic-save" ? "upstream commits are intentionally unpublished" : "network install mode is disabled"}.`);
|
|
1037
|
+
return { status: "passed", command: "treeseed structural lockfile validation", issues: [], error: null };
|
|
1036
1038
|
}
|
|
1037
1039
|
try {
|
|
1038
1040
|
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: lockfileValidationTimeoutMs(node, options), emitOutputOnSuccess: false });
|
|
@@ -1773,8 +1775,9 @@ async function saveOneRepository(node, options, state) {
|
|
|
1773
1775
|
return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
1774
1776
|
}));
|
|
1775
1777
|
const gitDependencyRefreshReferences = [...state.finalizedReferences.values()].filter((reference) => reference.mode === "dev-git-commit" && directDependencyNames.has(reference.packageName));
|
|
1778
|
+
const deferredGitDependencyValidation = options.deferPushUntilVerified === true && gitDependencyRefreshReferences.length > 0;
|
|
1776
1779
|
const lockfileGitDependenciesSynced = syncDirectGitDependencyLockfileEntries(node, options, gitDependencyRefreshReferences);
|
|
1777
|
-
if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json")))) {
|
|
1780
|
+
if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json"))) && !deferredGitDependencyValidation) {
|
|
1778
1781
|
validateStandaloneGitDependencyLockfile(node, options);
|
|
1779
1782
|
}
|
|
1780
1783
|
const gitDependencyRefreshSpecs = lockfileGitDependenciesSynced ? [] : gitDependencyRefreshReferences.map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
|
|
@@ -1809,7 +1812,7 @@ async function saveOneRepository(node, options, state) {
|
|
|
1809
1812
|
} else if (node.kind === "project" && (dependencyChanged || node.path === options.root && submodulesChanged) && hasNpmLockfile(node.path)) {
|
|
1810
1813
|
report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
|
|
1811
1814
|
}
|
|
1812
|
-
if (!isRootWorkspaceRepository(node, options) && hasNpmLockfile(node.path) && (packageNeedsVersion || dependencyChanged)) {
|
|
1815
|
+
if (!isRootWorkspaceRepository(node, options) && hasNpmLockfile(node.path) && (packageNeedsVersion || dependencyChanged) && !deferredGitDependencyValidation) {
|
|
1813
1816
|
validateStandaloneGitDependencyLockfile(node, options);
|
|
1814
1817
|
}
|
|
1815
1818
|
if (hasNpmLockfile(node.path) && (node.kind === "project" || packageNeedsVersion || dependencyChanged || submodulesChanged)) {
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
configuredRailwayServices,
|
|
44
44
|
findStaleTreeseedOperationsRunnerResources,
|
|
45
45
|
isTreeseedOperationsRunnerResourceName,
|
|
46
|
+
railwayLegacyAliasMigrationPolicy,
|
|
46
47
|
validateRailwayDeployPrerequisites
|
|
47
48
|
} from "../operations/services/railway-deploy.js";
|
|
48
49
|
import { shouldExposeManagedHostRuntimeSecret } from "../operations/services/managed-host-security.js";
|
|
@@ -3996,7 +3997,12 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
3996
3997
|
})),
|
|
3997
3998
|
database: databaseForIac
|
|
3998
3999
|
};
|
|
3999
|
-
const
|
|
4000
|
+
const aliasMigration = railwayLegacyAliasMigrationPolicy(scope, projectServices);
|
|
4001
|
+
const siblingResourceNames = [
|
|
4002
|
+
...configuredRailwaySiblingResourceNames(input, scope, project.name),
|
|
4003
|
+
...aliasMigration.retainedResourceNames
|
|
4004
|
+
];
|
|
4005
|
+
const { allowedResourceDeletions } = aliasMigration;
|
|
4000
4006
|
const {
|
|
4001
4007
|
applyRailwayIacProject,
|
|
4002
4008
|
cleanupRailwayIacRender,
|
|
@@ -4023,7 +4029,8 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
4023
4029
|
serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
|
|
4024
4030
|
service.serviceName,
|
|
4025
4031
|
service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
|
|
4026
|
-
]))
|
|
4032
|
+
])),
|
|
4033
|
+
allowedResourceDeletions
|
|
4027
4034
|
});
|
|
4028
4035
|
if (!validation.ok && effectiveIacInput.database && !effectiveIacInput.database.useNativePostgres && railwayIacPlanDeletesResource(plan.changeSet, effectiveIacInput.database.serviceName)) {
|
|
4029
4036
|
traceRailwayReconcile(topology.env, "sync:iac-native-postgres-adopt", `${project.name}/${environment.name}:${effectiveIacInput.database.serviceName}`);
|
|
@@ -4050,7 +4057,8 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
4050
4057
|
serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
|
|
4051
4058
|
service.serviceName,
|
|
4052
4059
|
service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
|
|
4053
|
-
]))
|
|
4060
|
+
])),
|
|
4061
|
+
allowedResourceDeletions
|
|
4054
4062
|
});
|
|
4055
4063
|
}
|
|
4056
4064
|
const retainedResources = selectRailwayIacRetainedResources(plan, siblingResourceNames);
|
|
@@ -4077,7 +4085,8 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
4077
4085
|
serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
|
|
4078
4086
|
service.serviceName,
|
|
4079
4087
|
service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
|
|
4080
|
-
]))
|
|
4088
|
+
])),
|
|
4089
|
+
allowedResourceDeletions
|
|
4081
4090
|
});
|
|
4082
4091
|
}
|
|
4083
4092
|
if (!validation.ok) {
|
|
@@ -66,6 +66,7 @@ export declare function validateRailwayIacChangeSet(changeSet: RailwayChangeSet
|
|
|
66
66
|
scope: string;
|
|
67
67
|
serviceSourceModes?: Record<string, string | null | undefined>;
|
|
68
68
|
serviceSourceRefs?: Record<string, string | null | undefined>;
|
|
69
|
+
allowedResourceDeletions?: string[];
|
|
69
70
|
}): RailwayIacValidationResult;
|
|
70
71
|
export declare function planRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacPlanResponse>;
|
|
71
72
|
export declare function applyRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacApplyResponse>;
|
|
@@ -255,10 +255,11 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
|
|
|
255
255
|
const blockedReasons = [];
|
|
256
256
|
const destructiveChanges = [];
|
|
257
257
|
const desired = /* @__PURE__ */ new Set([...desiredNames.services, ...desiredNames.volumes, ...desiredNames.database ? [desiredNames.database] : []]);
|
|
258
|
+
const allowedResourceDeletions = new Set(desiredNames.allowedResourceDeletions ?? []);
|
|
258
259
|
const created = new Set((changeSet?.changes ?? []).filter((change) => change.kind === "resource.create").map((change) => changeName(change)));
|
|
259
260
|
for (const change of changeSet?.changes ?? []) {
|
|
260
261
|
const name = changeName(change);
|
|
261
|
-
const serviceName = name.replace(/^(service|database)\./u, "");
|
|
262
|
+
const serviceName = name.replace(/^(service|database|volume)\./u, "");
|
|
262
263
|
const sourceMode = desiredNames.serviceSourceModes?.[name] ?? desiredNames.serviceSourceModes?.[serviceName] ?? null;
|
|
263
264
|
const sourceRef = desiredNames.serviceSourceRefs?.[name] ?? desiredNames.serviceSourceRefs?.[serviceName] ?? null;
|
|
264
265
|
const sourceChanged = change.kind === "resource.update" && isRailwaySourceChange(change);
|
|
@@ -269,7 +270,9 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
|
|
|
269
270
|
const apiPolicyService = isApiRailwaySourcePolicyService({ serviceName });
|
|
270
271
|
if (change.kind === "resource.delete") {
|
|
271
272
|
destructiveChanges.push(change.summary);
|
|
272
|
-
|
|
273
|
+
if (!allowedResourceDeletions.has(name) && !allowedResourceDeletions.has(serviceName)) {
|
|
274
|
+
blockedReasons.push(`Railway IaC plan would delete resource ${name || change.summary}; hosting reconciliation only deletes explicitly recognized obsolete aliases. Use the explicit destroy workflow for other deletions.`);
|
|
275
|
+
}
|
|
273
276
|
if (desired.has(name) && !created.has(name)) {
|
|
274
277
|
blockedReasons.push(`Railway IaC plan would delete desired resource ${name}.`);
|
|
275
278
|
}
|
|
@@ -1144,6 +1144,10 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
1144
1144
|
cached: boolean;
|
|
1145
1145
|
}[];
|
|
1146
1146
|
};
|
|
1147
|
+
productionImageRefs: {
|
|
1148
|
+
persisted: Record<string, string>;
|
|
1149
|
+
readiness: import("../workflow-support.js").TreeseedDeploymentReadinessReport;
|
|
1150
|
+
};
|
|
1147
1151
|
backMerge: {
|
|
1148
1152
|
packages: {
|
|
1149
1153
|
status: string;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
applyTreeseedSafeRepairs,
|
|
18
18
|
assertTreeseedCommandEnvironment,
|
|
19
19
|
checkTreeseedProviderConnections,
|
|
20
|
+
collectTreeseedEnvironmentContext,
|
|
20
21
|
collectTreeseedConfigContext,
|
|
21
22
|
collectTreeseedConfigSeedValues,
|
|
22
23
|
collectTreeseedPrintEnvReport,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
resolveTreeseedRemoteSession,
|
|
34
35
|
rotateTreeseedMachineKey,
|
|
35
36
|
setTreeseedRemoteSession,
|
|
37
|
+
setTreeseedMachineEnvironmentValue,
|
|
36
38
|
writeTreeseedMachineConfig
|
|
37
39
|
} from "../operations/services/config-runtime.js";
|
|
38
40
|
import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
@@ -746,6 +748,20 @@ function productionReleaseImageRefVersions(root, selectedVersions) {
|
|
|
746
748
|
}
|
|
747
749
|
return versions;
|
|
748
750
|
}
|
|
751
|
+
function persistProductionReleaseImageRefs(root, releaseImageRefs) {
|
|
752
|
+
const registry = collectTreeseedEnvironmentContext(root);
|
|
753
|
+
const entries = new Map(registry.entries.map((entry) => [entry.id, entry]));
|
|
754
|
+
const persisted = {};
|
|
755
|
+
for (const [id, value] of Object.entries(releaseImageRefs)) {
|
|
756
|
+
const entry = entries.get(id) ?? { id, storage: "scoped", sensitivity: "plain" };
|
|
757
|
+
if (entry.sensitivity === "secret") {
|
|
758
|
+
workflowError("release", "validation_failed", `Production release image ref ${id} must be a non-secret environment value.`);
|
|
759
|
+
}
|
|
760
|
+
setTreeseedMachineEnvironmentValue(root, "prod", entry, value);
|
|
761
|
+
persisted[id] = value;
|
|
762
|
+
}
|
|
763
|
+
return persisted;
|
|
764
|
+
}
|
|
749
765
|
function stableVersionLine(version) {
|
|
750
766
|
const match = version.match(/^(\d+\.\d+)\.\d+(?:-[0-9A-Za-z.-]+)?$/u);
|
|
751
767
|
return match?.[1] ?? null;
|
|
@@ -2225,7 +2241,7 @@ function runReleaseNpmInstall(repoDir, options = {}) {
|
|
|
2225
2241
|
if (shouldSkipReleaseInstall()) {
|
|
2226
2242
|
return { status: "skipped", reason: "disabled" };
|
|
2227
2243
|
}
|
|
2228
|
-
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2244
|
+
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2229
2245
|
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
2230
2246
|
let lastDetail = "";
|
|
2231
2247
|
for (let attempt = 1; attempt <= 10; attempt += 1) {
|
|
@@ -4722,7 +4738,7 @@ async function workflowSave(helpers, input) {
|
|
|
4722
4738
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
4723
4739
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
4724
4740
|
workflowRunId: workflowRun.runId,
|
|
4725
|
-
deferPushUntilVerified:
|
|
4741
|
+
deferPushUntilVerified: true,
|
|
4726
4742
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
4727
4743
|
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
4728
4744
|
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
@@ -5695,10 +5711,12 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5695
5711
|
{ root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
|
|
5696
5712
|
)) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
|
|
5697
5713
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
5698
|
-
|
|
5699
|
-
|
|
5714
|
+
if (!managedWorkflowWorktreeMetadata(root)) {
|
|
5715
|
+
for (const repo of checkedOutStagePromotionRepos(root)) {
|
|
5716
|
+
syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
|
|
5717
|
+
}
|
|
5718
|
+
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5700
5719
|
}
|
|
5701
|
-
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5702
5720
|
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" });
|
|
5703
5721
|
const payload = {
|
|
5704
5722
|
...basePayload,
|
|
@@ -6000,6 +6018,7 @@ ${blockers.join("\n")}`, {
|
|
|
6000
6018
|
},
|
|
6001
6019
|
{ id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6002
6020
|
{ 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 },
|
|
6021
|
+
{ 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 },
|
|
6003
6022
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
6004
6023
|
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6005
6024
|
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
@@ -6132,6 +6151,26 @@ ${rendered}`);
|
|
|
6132
6151
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6133
6152
|
}).then((workflowGates) => ({ workflowGates }));
|
|
6134
6153
|
});
|
|
6154
|
+
const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
|
|
6155
|
+
const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
|
|
6156
|
+
if (helpers.context.env) {
|
|
6157
|
+
Object.assign(helpers.context.env, persisted);
|
|
6158
|
+
Object.assign(process.env, persisted);
|
|
6159
|
+
}
|
|
6160
|
+
const readiness = await withContextEnv(persisted, () => collectTreeseedDeploymentReadiness({
|
|
6161
|
+
tenantRoot: root,
|
|
6162
|
+
environment: "prod",
|
|
6163
|
+
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
6164
|
+
}));
|
|
6165
|
+
const failures = readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
|
|
6166
|
+
if (failures.length > 0) {
|
|
6167
|
+
workflowError("release", "hosted_live_verification_failed", `Production readiness failed after persisting released image refs:
|
|
6168
|
+
${failures.join("\n")}`, {
|
|
6169
|
+
details: { releaseImageRefs: persisted, readiness }
|
|
6170
|
+
});
|
|
6171
|
+
}
|
|
6172
|
+
return { persisted, readiness };
|
|
6173
|
+
});
|
|
6135
6174
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
6136
6175
|
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6137
6176
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
@@ -6205,6 +6244,7 @@ ${rendered}`);
|
|
|
6205
6244
|
publishWait: publishWait.workflowGates,
|
|
6206
6245
|
publishedArtifacts,
|
|
6207
6246
|
productionPackageDeployWorkflows,
|
|
6247
|
+
productionImageRefs,
|
|
6208
6248
|
backMerge,
|
|
6209
6249
|
workspaceLinks,
|
|
6210
6250
|
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|
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"],
|