@treeseed/sdk 0.12.59 → 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/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/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,
|
|
@@ -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
|
}
|
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"],
|