@treeseed/sdk 0.12.43 → 0.12.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/guarantees/index.d.ts +83 -0
- package/dist/guarantees/index.js +688 -86
- package/dist/hosting/graph.js +40 -6
- package/dist/operations/services/git-workflow.d.ts +16 -1
- package/dist/operations/services/git-workflow.js +65 -5
- package/dist/operations/services/github-api.js +0 -19
- package/dist/operations/services/hosted-service-checks.js +17 -1
- package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
- package/dist/operations/services/live-hosted-service-checks.js +7 -4
- package/dist/operations/services/local-cleanup.js +3 -7
- package/dist/operations/services/package-adapters.d.ts +14 -0
- package/dist/operations/services/package-adapters.js +36 -3
- package/dist/operations/services/package-artifacts.d.ts +37 -0
- package/dist/operations/services/package-artifacts.js +99 -0
- package/dist/operations/services/railway-deploy.js +78 -18
- package/dist/operations/services/railway-source-policy.d.ts +19 -0
- package/dist/operations/services/railway-source-policy.js +66 -0
- package/dist/operations/services/repository-save-orchestrator.js +12 -5
- package/dist/platform/desired-state.js +2 -3
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/providers/railway-iac.d.ts +2 -0
- package/dist/reconcile/providers/railway-iac.js +31 -3
- package/dist/scenes/builtin-plugins.js +36 -5
- package/dist/scenes/device-matrix.js +2 -0
- package/dist/scenes/environment.js +1 -1
- package/dist/scenes/runner.js +28 -16
- package/dist/scenes/schema.js +31 -2
- package/dist/scenes/types.d.ts +25 -2
- package/dist/scenes/visual-audit-fixtures.js +9 -3
- package/dist/workflow/operations.d.ts +27 -92
- package/dist/workflow/operations.js +236 -144
- package/dist/workflow/runs.d.ts +1 -0
- package/dist/workflow/runs.js +57 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +4 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { runTreeseedGitText } from "./git-runner.js";
|
|
6
|
+
function readPackageJson(packageRoot) {
|
|
7
|
+
const path = resolve(packageRoot, "package.json");
|
|
8
|
+
if (!existsSync(path)) throw new Error(`Package artifact build requires ${path}.`);
|
|
9
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
function fileDigest(path) {
|
|
12
|
+
const bytes = readFileSync(path);
|
|
13
|
+
return {
|
|
14
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
15
|
+
size: bytes.byteLength
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function buildTreeseedPackageArtifact(input) {
|
|
19
|
+
const packageRoot = resolve(input.packageRoot);
|
|
20
|
+
const outputDir = resolve(input.outputDir);
|
|
21
|
+
const pkg = readPackageJson(packageRoot);
|
|
22
|
+
const packageName = typeof pkg.name === "string" ? pkg.name : "";
|
|
23
|
+
const packageVersion = typeof pkg.version === "string" ? pkg.version : "";
|
|
24
|
+
if (!packageName || !packageVersion) throw new Error("Package artifact build requires package name and version.");
|
|
25
|
+
mkdirSync(outputDir, { recursive: true });
|
|
26
|
+
const result = spawnSync("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", outputDir], {
|
|
27
|
+
cwd: packageRoot,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
+
env: process.env
|
|
31
|
+
});
|
|
32
|
+
if ((result.status ?? 1) !== 0) throw new Error(`npm pack failed for ${packageName}.
|
|
33
|
+
${result.stderr || result.stdout}`);
|
|
34
|
+
const output = JSON.parse(result.stdout);
|
|
35
|
+
const filename = output[0]?.filename;
|
|
36
|
+
if (!filename) throw new Error(`npm pack did not report an artifact for ${packageName}.`);
|
|
37
|
+
const artifactPath = resolve(outputDir, basename(filename));
|
|
38
|
+
if (!existsSync(artifactPath)) throw new Error(`npm pack artifact is missing: ${artifactPath}`);
|
|
39
|
+
const sourceSha = runTreeseedGitText(["rev-parse", "HEAD"], { cwd: packageRoot, mode: "read" }).trim();
|
|
40
|
+
const digest = fileDigest(artifactPath);
|
|
41
|
+
const manifest = {
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
kind: "treeseed.package-artifact",
|
|
44
|
+
packageName,
|
|
45
|
+
packageVersion,
|
|
46
|
+
sourceSha,
|
|
47
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48
|
+
file: basename(artifactPath),
|
|
49
|
+
sha256: digest.sha256,
|
|
50
|
+
size: digest.size
|
|
51
|
+
};
|
|
52
|
+
const manifestPath = resolve(outputDir, "manifest.json");
|
|
53
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
54
|
+
`, "utf8");
|
|
55
|
+
return { artifactPath, manifestPath, manifest };
|
|
56
|
+
}
|
|
57
|
+
function verifyTreeseedPackageArtifact(input) {
|
|
58
|
+
const manifestPath = resolve(input.manifestPath);
|
|
59
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
60
|
+
if (manifest.schemaVersion !== 1 || manifest.kind !== "treeseed.package-artifact") {
|
|
61
|
+
throw new Error(`Unsupported package artifact manifest: ${manifestPath}`);
|
|
62
|
+
}
|
|
63
|
+
const artifactPath = resolve(input.artifactPath ?? resolve(dirname(manifestPath), manifest.file));
|
|
64
|
+
if (!existsSync(artifactPath)) throw new Error(`Package artifact is missing: ${artifactPath}`);
|
|
65
|
+
const digest = fileDigest(artifactPath);
|
|
66
|
+
if (digest.sha256 !== manifest.sha256 || digest.size !== manifest.size) {
|
|
67
|
+
throw new Error(`Package artifact integrity check failed for ${artifactPath}.`);
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, artifactPath, manifestPath, manifest };
|
|
70
|
+
}
|
|
71
|
+
function hydrateTreeseedPackageArtifacts(input) {
|
|
72
|
+
const artifactsRoot = resolve(input.artifactsRoot);
|
|
73
|
+
const projectRoot = resolve(input.projectRoot);
|
|
74
|
+
const manifests = readdirSync(artifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(resolve(artifactsRoot, entry.name, "manifest.json"))).map((entry) => verifyTreeseedPackageArtifact({ manifestPath: resolve(artifactsRoot, entry.name, "manifest.json") })).sort((left, right) => left.manifest.packageName.localeCompare(right.manifest.packageName));
|
|
75
|
+
if (manifests.length === 0) throw new Error(`No package artifact manifests found under ${artifactsRoot}.`);
|
|
76
|
+
for (const entry of manifests) {
|
|
77
|
+
const packagePath = resolve(projectRoot, "node_modules", ...entry.manifest.packageName.split("/"));
|
|
78
|
+
rmSync(packagePath, { recursive: true, force: true });
|
|
79
|
+
mkdirSync(packagePath, { recursive: true });
|
|
80
|
+
const result = spawnSync("tar", ["-xzf", entry.artifactPath, "--strip-components=1", "-C", packagePath], {
|
|
81
|
+
cwd: projectRoot,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
84
|
+
env: process.env
|
|
85
|
+
});
|
|
86
|
+
if ((result.status ?? 1) !== 0) throw new Error(`Candidate artifact extraction failed for ${entry.manifest.packageName}.
|
|
87
|
+
${result.stderr || result.stdout}`);
|
|
88
|
+
const installed = readPackageJson(packagePath);
|
|
89
|
+
if (installed.version !== entry.manifest.packageVersion) {
|
|
90
|
+
throw new Error(`Hydrated ${entry.manifest.packageName} version mismatch: expected ${entry.manifest.packageVersion}, observed ${String(installed.version)}.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, projectRoot, artifactsRoot, packages: manifests.map((entry) => entry.manifest) };
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
buildTreeseedPackageArtifact,
|
|
97
|
+
hydrateTreeseedPackageArtifacts,
|
|
98
|
+
verifyTreeseedPackageArtifact
|
|
99
|
+
};
|
|
@@ -6,6 +6,7 @@ import { resolveTreeseedMachineEnvironmentValues } from "./config-runtime.js";
|
|
|
6
6
|
import { createPersistentDeployTarget, resolveTreeseedResourceIdentity } from "./deploy.js";
|
|
7
7
|
import { classifyTreeseedGitMode, runTreeseedGitText } from "./git-runner.js";
|
|
8
8
|
import { discoverTreeseedApplications } from "../../hosting/apps.js";
|
|
9
|
+
import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "./railway-source-policy.js";
|
|
9
10
|
import { runPrefixedCommand, sleep } from "./bootstrap-runner.js";
|
|
10
11
|
import {
|
|
11
12
|
ensureRailwayEnvironment,
|
|
@@ -142,6 +143,15 @@ function configuredEnvValue(env, name) {
|
|
|
142
143
|
const value = env?.[name];
|
|
143
144
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
144
145
|
}
|
|
146
|
+
function configuredApiPublicBaseUrl(deployConfig, scope) {
|
|
147
|
+
const apiSurface = deployConfig.surfaces?.api;
|
|
148
|
+
if (!apiSurface || typeof apiSurface !== "object") return null;
|
|
149
|
+
const environment = apiSurface.environments?.[scope];
|
|
150
|
+
const configured = environment?.baseUrl ?? environment?.domain ?? (scope === "local" ? apiSurface.localBaseUrl : null) ?? apiSurface.publicBaseUrl ?? null;
|
|
151
|
+
if (typeof configured !== "string" || !configured.trim()) return null;
|
|
152
|
+
const value = configured.trim().replace(/\/+$/u, "");
|
|
153
|
+
return /^https?:\/\//iu.test(value) ? value : `https://${value}`;
|
|
154
|
+
}
|
|
145
155
|
function railwayDeployTransport(env) {
|
|
146
156
|
const configured = configuredEnvValue(env, "TREESEED_RAILWAY_DEPLOY_TRANSPORT").toLowerCase();
|
|
147
157
|
return configured === "cli-fallback" ? "cli-fallback" : "api";
|
|
@@ -672,7 +682,7 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
|
|
|
672
682
|
normalizedScope,
|
|
673
683
|
service.environments?.[normalizedScope]?.railwayEnvironment
|
|
674
684
|
);
|
|
675
|
-
const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? null;
|
|
685
|
+
const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? (serviceKey === "api" ? configuredApiPublicBaseUrl(deployConfig, normalizedScope) : null);
|
|
676
686
|
const configuredServiceName = service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`);
|
|
677
687
|
const configuredRunnerPool = service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
|
|
678
688
|
const runnerPool = serviceKey === "operationsRunner" || serviceKey === "capacityProviderRunner" ? {
|
|
@@ -721,7 +731,7 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
|
|
|
721
731
|
sourceBranch: sourcePolicy.sourceBranch,
|
|
722
732
|
sourceCommit: sourcePolicy.sourceCommit,
|
|
723
733
|
sourceRootDirectory: sourcePolicy.sourceRootDirectory,
|
|
724
|
-
dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ??
|
|
734
|
+
dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName }) : null,
|
|
725
735
|
healthcheckPath: service.railway?.healthcheckPath ?? null,
|
|
726
736
|
healthcheckTimeoutSeconds: service.railway?.healthcheckTimeoutSeconds ?? null,
|
|
727
737
|
healthcheckIntervalSeconds: service.railway?.healthcheckIntervalSeconds ?? null,
|
|
@@ -764,6 +774,9 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
|
|
|
764
774
|
const configuredSource = railway.source && typeof railway.source === "object" && !Array.isArray(railway.source) ? railway.source : {};
|
|
765
775
|
const treeDxRoot = resolve(workspaceRoot ?? tenantRoot, "packages", "treedx");
|
|
766
776
|
const configuredMode = typeof railway.sourceMode === "string" ? railway.sourceMode : null;
|
|
777
|
+
if (scope === "staging" && configuredMode === "image") {
|
|
778
|
+
throw new Error("public-treedx-node-01: API Railway staging services must use GitHub Dockerfile source builds (configured sourceMode image is not allowed).");
|
|
779
|
+
}
|
|
767
780
|
const sourceMode = scope === "prod" ? "image" : configuredMode === "git" || configuredMode === "image" ? configuredMode : "git";
|
|
768
781
|
const repository = typeof railway.sourceRepo === "string" ? railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readTreeseedPackageRepository(treeDxRoot) ?? "treeseed-ai/treedx";
|
|
769
782
|
const sourceBranch = typeof railway.sourceBranch === "string" ? railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : scope === "staging" ? "staging" : null;
|
|
@@ -774,8 +787,22 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
|
|
|
774
787
|
return Array.from({ length: bootstrapCount }, (_, offset) => {
|
|
775
788
|
const index = offset + 1;
|
|
776
789
|
const serviceName = `${PUBLIC_TREEDX_NODE_SERVICE_KEY_PREFIX}${String(index).padStart(2, "0")}`;
|
|
777
|
-
|
|
790
|
+
const service = {
|
|
778
791
|
key: serviceName,
|
|
792
|
+
serviceName,
|
|
793
|
+
sourceMode,
|
|
794
|
+
sourceRepo: sourceMode === "git" ? repository : null,
|
|
795
|
+
sourceBranch: sourceMode === "git" ? sourceBranch : null,
|
|
796
|
+
sourceCommit: sourceMode === "git" ? typeof railway.sourceCommit === "string" ? railway.sourceCommit : typeof configuredSource.commit === "string" ? configuredSource.commit : headCommitSafe(treeDxRoot) ?? headCommitSafe(tenantRoot) : null,
|
|
797
|
+
sourceRootDirectory: sourceMode === "git" ? sourceRootDirectory : null,
|
|
798
|
+
imageRef: sourceMode === "image" ? baseImageRef : null,
|
|
799
|
+
dockerfilePath: sourceMode === "git" ? railway.dockerfilePath ?? "/Dockerfile" : null,
|
|
800
|
+
buildCommand: sourceMode === "git" ? railway.buildCommand ?? null : null,
|
|
801
|
+
startCommand: sourceMode === "git" ? railway.startCommand ?? null : null
|
|
802
|
+
};
|
|
803
|
+
assertApiRailwaySourcePolicy(scope, service);
|
|
804
|
+
return {
|
|
805
|
+
key: service.key,
|
|
779
806
|
instanceKey: serviceName,
|
|
780
807
|
runnerIndex: null,
|
|
781
808
|
serviceConfig: null,
|
|
@@ -783,20 +810,20 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
|
|
|
783
810
|
projectId: typeof railway.projectId === "string" ? railway.projectId : null,
|
|
784
811
|
projectName,
|
|
785
812
|
serviceId: typeof railway.serviceId === "string" && bootstrapCount === 1 ? railway.serviceId : null,
|
|
786
|
-
serviceName,
|
|
813
|
+
serviceName: service.serviceName,
|
|
787
814
|
runnerId: null,
|
|
788
815
|
rootDir: treeDxRoot,
|
|
789
816
|
publicBaseUrl: null,
|
|
790
817
|
railwayEnvironment,
|
|
791
|
-
buildCommand:
|
|
792
|
-
startCommand:
|
|
793
|
-
imageRef:
|
|
794
|
-
sourceMode,
|
|
795
|
-
sourceRepo:
|
|
796
|
-
sourceBranch:
|
|
797
|
-
sourceCommit:
|
|
798
|
-
sourceRootDirectory:
|
|
799
|
-
dockerfilePath:
|
|
818
|
+
buildCommand: service.buildCommand,
|
|
819
|
+
startCommand: service.startCommand,
|
|
820
|
+
imageRef: service.imageRef,
|
|
821
|
+
sourceMode: service.sourceMode,
|
|
822
|
+
sourceRepo: service.sourceRepo,
|
|
823
|
+
sourceBranch: service.sourceBranch,
|
|
824
|
+
sourceCommit: service.sourceCommit,
|
|
825
|
+
sourceRootDirectory: service.sourceRootDirectory,
|
|
826
|
+
dockerfilePath: service.dockerfilePath,
|
|
800
827
|
healthcheckPath: railway.healthcheckPath ?? null,
|
|
801
828
|
healthcheckTimeoutSeconds: railway.healthcheckTimeoutSeconds ?? null,
|
|
802
829
|
healthcheckIntervalSeconds: railway.healthcheckIntervalSeconds ?? null,
|
|
@@ -870,25 +897,48 @@ function resolveRailwayServiceSourcePolicy({ tenantRoot, scope, serviceKey, serv
|
|
|
870
897
|
const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
|
|
871
898
|
const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
|
|
872
899
|
const configuredRepo = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : null;
|
|
873
|
-
const
|
|
900
|
+
const serviceName = service.railway?.serviceName ?? null;
|
|
901
|
+
const packageRepository = configuredRepo ?? readTreeseedPackageRepository(serviceRoot) ?? readTreeseedPackageRepository(tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
|
|
902
|
+
const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
|
|
874
903
|
const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
|
|
904
|
+
if (scope === "staging" && isApiRailwaySourcePolicyService({ key: serviceKey, serviceName }) && (configuredMode === "image" || service.railway?.imageRef)) {
|
|
905
|
+
throw new Error(`${serviceName ?? serviceKey}: API Railway staging services must use GitHub Dockerfile source builds (configured image source is not allowed).`);
|
|
906
|
+
}
|
|
875
907
|
const sourceMode = scope === "prod" ? "image" : scope === "staging" && apiPackageSourceEligible ? "git" : configuredMode === "git" || configuredMode === "image" ? configuredMode : imageRef ? "image" : "git";
|
|
876
908
|
if (sourceMode !== "git") {
|
|
877
|
-
|
|
909
|
+
const policy2 = {
|
|
878
910
|
sourceMode: "image",
|
|
879
911
|
sourceRepo: null,
|
|
880
912
|
sourceBranch: null,
|
|
881
913
|
sourceCommit: null,
|
|
882
914
|
sourceRootDirectory: null
|
|
883
915
|
};
|
|
916
|
+
assertApiRailwaySourcePolicy(scope, {
|
|
917
|
+
key: serviceKey,
|
|
918
|
+
serviceName,
|
|
919
|
+
imageRef,
|
|
920
|
+
dockerfilePath: null,
|
|
921
|
+
buildCommand: null,
|
|
922
|
+
startCommand: null,
|
|
923
|
+
...policy2
|
|
924
|
+
});
|
|
925
|
+
return policy2;
|
|
884
926
|
}
|
|
885
|
-
|
|
927
|
+
const policy = {
|
|
886
928
|
sourceMode: "git",
|
|
887
929
|
sourceRepo: packageRepository,
|
|
888
930
|
sourceBranch: typeof service.railway?.sourceBranch === "string" ? service.railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : scope === "staging" ? "staging" : null,
|
|
889
|
-
sourceCommit: headCommitSafe(serviceRoot),
|
|
931
|
+
sourceCommit: typeof service.railway?.sourceCommit === "string" ? service.railway.sourceCommit : typeof configuredSource.commit === "string" ? configuredSource.commit : headCommitSafe(serviceRoot),
|
|
890
932
|
sourceRootDirectory: typeof service.railway?.sourceRootDirectory === "string" ? service.railway.sourceRootDirectory : typeof configuredSource.rootDirectory === "string" ? configuredSource.rootDirectory : "."
|
|
891
933
|
};
|
|
934
|
+
assertApiRailwaySourcePolicy(scope, {
|
|
935
|
+
key: serviceKey,
|
|
936
|
+
serviceName,
|
|
937
|
+
imageRef: null,
|
|
938
|
+
dockerfilePath,
|
|
939
|
+
...policy
|
|
940
|
+
});
|
|
941
|
+
return policy;
|
|
892
942
|
}
|
|
893
943
|
function resolveRailwayCapacityProviderRoot(tenantRoot, service) {
|
|
894
944
|
if (service.railway?.rootDir) {
|
|
@@ -1533,6 +1583,17 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
|
|
|
1533
1583
|
serviceId: railwayService.id,
|
|
1534
1584
|
variables: {
|
|
1535
1585
|
TREESEED_SKIP_PACKAGE_PREPARE: "1",
|
|
1586
|
+
...["api", "operationsRunner"].includes(service.key) ? {
|
|
1587
|
+
...configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET") ? {
|
|
1588
|
+
TREESEED_PLATFORM_RUNNER_SECRET: configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET")
|
|
1589
|
+
} : {},
|
|
1590
|
+
...configuredEnvValue(env, "TREESEED_CREDENTIAL_SESSION_SECRET") ? {
|
|
1591
|
+
TREESEED_CREDENTIAL_SESSION_SECRET: configuredEnvValue(env, "TREESEED_CREDENTIAL_SESSION_SECRET")
|
|
1592
|
+
} : {},
|
|
1593
|
+
...configuredEnvValue(env, "TREESEED_WEB_SERVICE_SECRET") ? {
|
|
1594
|
+
TREESEED_WEB_SERVICE_SECRET: configuredEnvValue(env, "TREESEED_WEB_SERVICE_SECRET")
|
|
1595
|
+
} : {}
|
|
1596
|
+
} : {},
|
|
1536
1597
|
...service.sourceMode === "git" ? {
|
|
1537
1598
|
TREESEED_DEPLOY_SOURCE_MODE: "git",
|
|
1538
1599
|
...service.sourceRepo ? { TREESEED_DEPLOY_SOURCE_REPOSITORY: service.sourceRepo } : {},
|
|
@@ -1550,7 +1611,6 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
|
|
|
1550
1611
|
TREESEED_MANAGER_ID: normalizeScope(service.scope),
|
|
1551
1612
|
...configuredEnvValue(env, "TREESEED_RAILWAY_API_TOKEN") ? { TREESEED_RAILWAY_API_TOKEN: configuredEnvValue(env, "TREESEED_RAILWAY_API_TOKEN") } : {},
|
|
1552
1613
|
...configuredEnvValue(env, "TREESEED_RAILWAY_WORKSPACE") ? { TREESEED_RAILWAY_WORKSPACE: configuredEnvValue(env, "TREESEED_RAILWAY_WORKSPACE") } : {},
|
|
1553
|
-
...configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET") ? { TREESEED_PLATFORM_RUNNER_SECRET: configuredEnvValue(env, "TREESEED_PLATFORM_RUNNER_SECRET") } : {},
|
|
1554
1614
|
...configuredEnvValue(env, "TREESEED_API_BASE_URL") || configuredEnvValue(env, "TREESEED_URL") ? {
|
|
1555
1615
|
TREESEED_API_BASE_URL: configuredEnvValue(env, "TREESEED_API_BASE_URL") || configuredEnvValue(env, "TREESEED_URL")
|
|
1556
1616
|
} : {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type TreeseedRailwaySourcePolicyScope = 'local' | 'staging' | 'prod';
|
|
2
|
+
export type TreeseedRailwaySourcePolicyService = {
|
|
3
|
+
key?: string | null;
|
|
4
|
+
serviceName?: string | null;
|
|
5
|
+
sourceMode?: string | null;
|
|
6
|
+
sourceRepo?: string | null;
|
|
7
|
+
sourceBranch?: string | null;
|
|
8
|
+
sourceCommit?: string | null;
|
|
9
|
+
sourceRootDirectory?: string | null;
|
|
10
|
+
imageRef?: string | null;
|
|
11
|
+
dockerfilePath?: string | null;
|
|
12
|
+
buildCommand?: string | null;
|
|
13
|
+
startCommand?: string | null;
|
|
14
|
+
};
|
|
15
|
+
export declare function isApiRailwaySourcePolicyService(service: TreeseedRailwaySourcePolicyService): boolean;
|
|
16
|
+
export declare function isImmutableRailwayImageRef(value: unknown): boolean;
|
|
17
|
+
export declare function apiRailwayDefaultSourceRepo(service: TreeseedRailwaySourcePolicyService): "treeseed-ai/treedx" | "treeseed-ai/api" | null;
|
|
18
|
+
export declare function apiRailwayDefaultDockerfilePath(service: TreeseedRailwaySourcePolicyService): "/Dockerfile.api" | "/Dockerfile.operations-runner" | "/Dockerfile" | null;
|
|
19
|
+
export declare function assertApiRailwaySourcePolicy(scope: TreeseedRailwaySourcePolicyScope | string, service: TreeseedRailwaySourcePolicyService): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function isApiRailwaySourcePolicyService(service) {
|
|
2
|
+
const key = String(service.key ?? "").trim();
|
|
3
|
+
const serviceName = String(service.serviceName ?? "").trim();
|
|
4
|
+
return key.startsWith("public-treedx-node-") || serviceName === "treeseed-api" || /^treeseed-api-operations-runner-\d+$/u.test(serviceName) || /^public-treedx-node-\d+$/u.test(serviceName);
|
|
5
|
+
}
|
|
6
|
+
function isImmutableRailwayImageRef(value) {
|
|
7
|
+
const imageRef = typeof value === "string" ? value.trim() : "";
|
|
8
|
+
if (!imageRef || !imageRef.includes(":")) return false;
|
|
9
|
+
const tag = imageRef.split(":").pop()?.trim() ?? "";
|
|
10
|
+
return Boolean(tag) && !["latest", "staging", "dev", "local"].includes(tag);
|
|
11
|
+
}
|
|
12
|
+
function apiRailwayDefaultSourceRepo(service) {
|
|
13
|
+
const serviceName = String(service.serviceName ?? "").trim();
|
|
14
|
+
if (serviceName === "treeseed-api" || /^treeseed-api-operations-runner-\d+$/u.test(serviceName)) return "treeseed-ai/api";
|
|
15
|
+
if (/^public-treedx-node-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "treeseed-ai/treedx";
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function apiRailwayDefaultDockerfilePath(service) {
|
|
19
|
+
const serviceName = String(service.serviceName ?? "").trim();
|
|
20
|
+
if (serviceName === "treeseed-api") return "/Dockerfile.api";
|
|
21
|
+
if (/^treeseed-api-operations-runner-\d+$/u.test(serviceName)) return "/Dockerfile.operations-runner";
|
|
22
|
+
if (/^public-treedx-node-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "/Dockerfile";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function assertApiRailwaySourcePolicy(scope, service) {
|
|
26
|
+
if (!isApiRailwaySourcePolicyService(service)) return;
|
|
27
|
+
const normalizedScope = scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
|
|
28
|
+
const label = service.serviceName ?? service.key ?? "Railway service";
|
|
29
|
+
if (normalizedScope === "staging") {
|
|
30
|
+
const issues = [
|
|
31
|
+
service.sourceMode === "git" ? null : "sourceMode must be git",
|
|
32
|
+
service.imageRef ? "imageRef must be empty" : null,
|
|
33
|
+
service.sourceRepo ? null : "sourceRepo must be set",
|
|
34
|
+
service.sourceBranch === "staging" ? null : "sourceBranch must be staging",
|
|
35
|
+
service.sourceRootDirectory ? null : "sourceRootDirectory must be set",
|
|
36
|
+
service.dockerfilePath ? null : "dockerfilePath must be set"
|
|
37
|
+
].filter((issue) => Boolean(issue));
|
|
38
|
+
if (issues.length > 0) {
|
|
39
|
+
throw new Error(`${label}: API Railway staging services must use GitHub Dockerfile source builds (${issues.join("; ")}).`);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (normalizedScope === "prod") {
|
|
44
|
+
const issues = [
|
|
45
|
+
service.sourceMode === "image" ? null : "sourceMode must be image",
|
|
46
|
+
isImmutableRailwayImageRef(service.imageRef) ? null : "imageRef must be an immutable released image tag",
|
|
47
|
+
service.sourceRepo ? "sourceRepo must be empty" : null,
|
|
48
|
+
service.sourceBranch ? "sourceBranch must be empty" : null,
|
|
49
|
+
service.sourceCommit ? "sourceCommit must be empty" : null,
|
|
50
|
+
service.sourceRootDirectory ? "sourceRootDirectory must be empty" : null,
|
|
51
|
+
service.dockerfilePath ? "dockerfilePath must be empty" : null,
|
|
52
|
+
service.buildCommand ? "buildCommand must be empty" : null,
|
|
53
|
+
service.startCommand ? "startCommand must be empty" : null
|
|
54
|
+
].filter((issue) => Boolean(issue));
|
|
55
|
+
if (issues.length > 0) {
|
|
56
|
+
throw new Error(`${label}: API Railway production services must use released Docker image sources (${issues.join("; ")}).`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
apiRailwayDefaultDockerfilePath,
|
|
62
|
+
apiRailwayDefaultSourceRepo,
|
|
63
|
+
assertApiRailwaySourcePolicy,
|
|
64
|
+
isApiRailwaySourcePolicyService,
|
|
65
|
+
isImmutableRailwayImageRef
|
|
66
|
+
};
|
|
@@ -117,7 +117,7 @@ function npmCacheForCwd(cwd) {
|
|
|
117
117
|
return cacheDir;
|
|
118
118
|
}
|
|
119
119
|
function npmWorkflowEnv(env = {}, cwd = process.cwd()) {
|
|
120
|
-
const npmCache = env.npm_config_cache ?? env.NPM_CONFIG_CACHE ??
|
|
120
|
+
const npmCache = env.npm_config_cache ?? env.NPM_CONFIG_CACHE ?? npmCacheForCwd(cwd);
|
|
121
121
|
return withShortProcessTempEnv({
|
|
122
122
|
...env,
|
|
123
123
|
NPM_CONFIG_CACHE: npmCache,
|
|
@@ -777,7 +777,7 @@ function applyPackageVersion(node, version) {
|
|
|
777
777
|
return true;
|
|
778
778
|
}
|
|
779
779
|
function shouldSkipNetworkInstall() {
|
|
780
|
-
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE
|
|
780
|
+
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE !== "allow";
|
|
781
781
|
}
|
|
782
782
|
function shouldSkipGitDependencySmoke(options) {
|
|
783
783
|
return shouldSkipNetworkInstall() || process.env.TREESEED_GIT_DEPENDENCY_SMOKE === "skip" || options?.verifyMode === "skip";
|
|
@@ -948,6 +948,11 @@ function lockfileValidationCommand(node, options) {
|
|
|
948
948
|
const args = rootWorkspaceInstall ? ["ci", "--ignore-scripts", "--plan"] : ["ci", "--ignore-scripts", "--plan", "--workspaces=false"];
|
|
949
949
|
return { command: "npm", args };
|
|
950
950
|
}
|
|
951
|
+
function lockfileValidationTimeoutMs(node, options) {
|
|
952
|
+
const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
|
|
953
|
+
const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
|
|
954
|
+
return rootWorkspaceInstall ? 18e5 : 6e5;
|
|
955
|
+
}
|
|
951
956
|
async function validateRepositoryLockfile(node, options) {
|
|
952
957
|
if (!hasNpmLockfile(node.path)) {
|
|
953
958
|
return { status: "skipped", command: null, issues: [], error: "no npm lockfile" };
|
|
@@ -973,7 +978,7 @@ async function validateRepositoryLockfile(node, options) {
|
|
|
973
978
|
return { status: "skipped", command: commandText, issues: [], error: "disabled" };
|
|
974
979
|
}
|
|
975
980
|
try {
|
|
976
|
-
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs:
|
|
981
|
+
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: lockfileValidationTimeoutMs(node, options), emitOutputOnSuccess: false });
|
|
977
982
|
const packageCount = npmLockfilePackageCount(node.path);
|
|
978
983
|
const countText = packageCount === null ? "package-lock entries" : `${packageCount} package${packageCount === 1 ? "" : "s"}`;
|
|
979
984
|
emitProgress(options, node, "lockfile", `Lockfile validation passed: ${countText} checked, 0 issues.`);
|
|
@@ -1124,7 +1129,8 @@ function pullRebaseFromOrigin(node, options, branch) {
|
|
|
1124
1129
|
};
|
|
1125
1130
|
}
|
|
1126
1131
|
try {
|
|
1127
|
-
runCapturedCommand(node, options, "rebase", "git", ["
|
|
1132
|
+
runCapturedCommand(node, options, "rebase", "git", ["fetch", "origin", `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1133
|
+
runCapturedCommand(node, options, "rebase", "git", ["rebase", `refs/remotes/origin/${branch}`]);
|
|
1128
1134
|
return {
|
|
1129
1135
|
remoteBranchExisted: true,
|
|
1130
1136
|
pulledRebase: true
|
|
@@ -1161,7 +1167,7 @@ function pushCurrentBranch(node, options, branch, tagName) {
|
|
|
1161
1167
|
async function finishRepositorySavePublish(node, options, state, report, input) {
|
|
1162
1168
|
const reference = input.reference ?? null;
|
|
1163
1169
|
const tagName = input.tagName ?? reference?.tagName ?? null;
|
|
1164
|
-
const shouldDeferPush = options.deferPushUntilVerified === true
|
|
1170
|
+
const shouldDeferPush = options.deferPushUntilVerified === true;
|
|
1165
1171
|
if (shouldDeferPush) {
|
|
1166
1172
|
state.deferredPushes.push({
|
|
1167
1173
|
node,
|
|
@@ -1828,6 +1834,7 @@ async function saveOneRepository(node, options, state) {
|
|
|
1828
1834
|
return report;
|
|
1829
1835
|
}
|
|
1830
1836
|
report.committed = true;
|
|
1837
|
+
report.commitSha = headCommit(node.path);
|
|
1831
1838
|
const rebase = pullRebaseFromOrigin(node, options, branch);
|
|
1832
1839
|
const verifyMode = options.verifyMode ?? "action-first";
|
|
1833
1840
|
if (node.kind === "project" && node.path === options.root && Array.isArray(node.packageJson?.workspaces)) {
|
|
@@ -652,9 +652,8 @@ function localDevelopmentResources(tenantRoot, environment, localContent) {
|
|
|
652
652
|
dependencies: [composeId],
|
|
653
653
|
spec: {
|
|
654
654
|
mode: "local",
|
|
655
|
-
roles: ["
|
|
656
|
-
volumePolicy: "shared-local"
|
|
657
|
-
healthEndpoint: "http://127.0.0.1:4783/healthz"
|
|
655
|
+
roles: ["manager", "runner"],
|
|
656
|
+
volumePolicy: "shared-local"
|
|
658
657
|
},
|
|
659
658
|
source: { type: "package-adapter", id: "@treeseed/agent" }
|
|
660
659
|
},
|
|
@@ -3952,7 +3952,11 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
3952
3952
|
volumes: rendered.volumeNames,
|
|
3953
3953
|
database: rendered.databaseName,
|
|
3954
3954
|
scope,
|
|
3955
|
-
serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null]))
|
|
3955
|
+
serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null])),
|
|
3956
|
+
serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
|
|
3957
|
+
service.serviceName,
|
|
3958
|
+
service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
|
|
3959
|
+
]))
|
|
3956
3960
|
});
|
|
3957
3961
|
if (!validation.ok && effectiveIacInput.database && !effectiveIacInput.database.useNativePostgres && railwayIacPlanDeletesResource(plan.changeSet, effectiveIacInput.database.serviceName)) {
|
|
3958
3962
|
traceRailwayReconcile(topology.env, "sync:iac-native-postgres-adopt", `${project.name}/${environment.name}:${effectiveIacInput.database.serviceName}`);
|
|
@@ -3975,7 +3979,11 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
|
|
|
3975
3979
|
volumes: rendered.volumeNames,
|
|
3976
3980
|
database: rendered.databaseName,
|
|
3977
3981
|
scope,
|
|
3978
|
-
serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null]))
|
|
3982
|
+
serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null])),
|
|
3983
|
+
serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
|
|
3984
|
+
service.serviceName,
|
|
3985
|
+
service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
|
|
3986
|
+
]))
|
|
3979
3987
|
});
|
|
3980
3988
|
}
|
|
3981
3989
|
if (!validation.ok) {
|
|
@@ -28,6 +28,7 @@ export type TreeseedRailwayIacDatabase = {
|
|
|
28
28
|
};
|
|
29
29
|
export type TreeseedRailwayIacProjectInput = {
|
|
30
30
|
tenantRoot: string;
|
|
31
|
+
scope?: string | null;
|
|
31
32
|
projectName: string;
|
|
32
33
|
projectId: string;
|
|
33
34
|
environmentName: string;
|
|
@@ -61,6 +62,7 @@ export declare function validateRailwayIacChangeSet(changeSet: RailwayChangeSet
|
|
|
61
62
|
database: string | null;
|
|
62
63
|
scope: string;
|
|
63
64
|
serviceSourceModes?: Record<string, string | null | undefined>;
|
|
65
|
+
serviceSourceRefs?: Record<string, string | null | undefined>;
|
|
64
66
|
}): RailwayIacValidationResult;
|
|
65
67
|
export declare function planRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacPlanResponse>;
|
|
66
68
|
export declare function applyRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacApplyResponse>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { runRailwayIac } from "railway/iac";
|
|
4
|
+
import { assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../../operations/services/railway-source-policy.js";
|
|
4
5
|
function js(value) {
|
|
5
6
|
return JSON.stringify(value);
|
|
6
7
|
}
|
|
@@ -97,7 +98,13 @@ function renderPostgresEnv() {
|
|
|
97
98
|
["RAILWAY_DEPLOYMENT_DRAINING_SECONDS", js("60")]
|
|
98
99
|
]);
|
|
99
100
|
}
|
|
101
|
+
function normalizeIacScope(input) {
|
|
102
|
+
if (input.scope === "prod" || input.scope === "staging") return input.scope;
|
|
103
|
+
const environmentName = String(input.environmentName ?? "").trim().toLowerCase();
|
|
104
|
+
return environmentName === "production" || environmentName === "prod" ? "prod" : environmentName === "staging" ? "staging" : "local";
|
|
105
|
+
}
|
|
100
106
|
function renderRailwayIacProject(input) {
|
|
107
|
+
const scope = normalizeIacScope(input);
|
|
101
108
|
const region = input.region?.trim() || "us-east4-eqdc4a";
|
|
102
109
|
const tempParent = resolve(input.tenantRoot, ".treeseed", "tmp");
|
|
103
110
|
mkdirSync(tempParent, { recursive: true });
|
|
@@ -145,6 +152,7 @@ function renderRailwayIacProject(input) {
|
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
input.services.forEach((service, index) => {
|
|
155
|
+
assertApiRailwaySourcePolicy(scope, service);
|
|
148
156
|
const serviceVar = id("svc", index);
|
|
149
157
|
const invalidVariables = validateGeneratedVariables(service);
|
|
150
158
|
if (invalidVariables.length > 0) {
|
|
@@ -232,7 +240,15 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
|
|
|
232
240
|
const created = new Set((changeSet?.changes ?? []).filter((change) => change.kind === "resource.create").map((change) => changeName(change)));
|
|
233
241
|
for (const change of changeSet?.changes ?? []) {
|
|
234
242
|
const name = changeName(change);
|
|
235
|
-
const
|
|
243
|
+
const serviceName = name.replace(/^(service|database)\./u, "");
|
|
244
|
+
const sourceMode = desiredNames.serviceSourceModes?.[name] ?? desiredNames.serviceSourceModes?.[serviceName] ?? null;
|
|
245
|
+
const sourceRef = desiredNames.serviceSourceRefs?.[name] ?? desiredNames.serviceSourceRefs?.[serviceName] ?? null;
|
|
246
|
+
const sourceChanged = change.kind === "resource.update" && isRailwaySourceChange(change);
|
|
247
|
+
const imageSourceChange = sourceChanged && isRailwayImageSourceChange(change);
|
|
248
|
+
const gitSourceChange = sourceChanged && isRailwayGitSourceChange(change);
|
|
249
|
+
const desiredGitSource = sourceMode === "git" && typeof sourceRef === "string" && sourceRef.startsWith("github:");
|
|
250
|
+
const desiredImageSource = sourceMode === "image" && typeof sourceRef === "string" && sourceRef.startsWith("image:");
|
|
251
|
+
const apiPolicyService = isApiRailwaySourcePolicyService({ serviceName });
|
|
236
252
|
if (change.kind === "resource.delete") {
|
|
237
253
|
destructiveChanges.push(change.summary);
|
|
238
254
|
blockedReasons.push(`Railway IaC plan would delete resource ${name || change.summary}; hosting reconciliation only updates or creates resources. Use the explicit destroy workflow for deletions.`);
|
|
@@ -240,12 +256,24 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
|
|
|
240
256
|
blockedReasons.push(`Railway IaC plan would delete desired resource ${name}.`);
|
|
241
257
|
}
|
|
242
258
|
}
|
|
243
|
-
if (desiredNames.scope === "staging" &&
|
|
259
|
+
if (desiredNames.scope === "staging" && sourceChanged && apiPolicyService && sourceMode === "git" && !gitSourceChange && !desiredGitSource) {
|
|
260
|
+
blockedReasons.push(`Railway IaC plan would change staging API resource ${name} source without confirming a GitHub source.`);
|
|
261
|
+
}
|
|
262
|
+
if (desiredNames.scope === "staging" && sourceChanged && imageSourceChange && !(apiPolicyService && sourceMode === "git" && (gitSourceChange || desiredGitSource))) {
|
|
244
263
|
blockedReasons.push(`Railway IaC plan would switch staging resource ${name} to an image source.`);
|
|
245
264
|
}
|
|
246
|
-
if (desiredNames.scope === "
|
|
265
|
+
if (desiredNames.scope === "staging" && sourceChanged && (!sourceMode || sourceMode === "image")) {
|
|
266
|
+
blockedReasons.push(`Railway IaC plan would apply an image-backed desired source to staging resource ${name}.`);
|
|
267
|
+
}
|
|
268
|
+
if (desiredNames.scope === "prod" && sourceChanged && apiPolicyService && sourceMode === "image" && !imageSourceChange && !desiredImageSource) {
|
|
269
|
+
blockedReasons.push(`Railway IaC plan would change production API resource ${name} source without confirming an image source.`);
|
|
270
|
+
}
|
|
271
|
+
if (desiredNames.scope === "prod" && sourceChanged && gitSourceChange) {
|
|
247
272
|
blockedReasons.push(`Railway IaC plan would switch production resource ${name} to a Git source.`);
|
|
248
273
|
}
|
|
274
|
+
if (desiredNames.scope === "prod" && sourceChanged && (!sourceMode || sourceMode === "git")) {
|
|
275
|
+
blockedReasons.push(`Railway IaC plan would apply a Git-backed desired source to production resource ${name}.`);
|
|
276
|
+
}
|
|
249
277
|
}
|
|
250
278
|
return {
|
|
251
279
|
ok: blockedReasons.length === 0,
|