@treeseed/sdk 0.12.44 → 0.12.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/guarantees/index.d.ts +83 -0
- package/dist/guarantees/index.js +688 -86
- package/dist/hosting/graph.js +40 -6
- package/dist/operations/services/git-workflow.d.ts +16 -1
- package/dist/operations/services/git-workflow.js +65 -5
- package/dist/operations/services/github-api.js +0 -19
- package/dist/operations/services/hosted-service-checks.js +17 -1
- package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
- package/dist/operations/services/live-hosted-service-checks.js +8 -5
- package/dist/operations/services/local-cleanup.js +3 -7
- package/dist/operations/services/package-adapters.d.ts +14 -0
- package/dist/operations/services/package-adapters.js +36 -3
- package/dist/operations/services/package-artifacts.d.ts +37 -0
- package/dist/operations/services/package-artifacts.js +99 -0
- package/dist/operations/services/railway-deploy.js +78 -18
- package/dist/operations/services/railway-source-policy.d.ts +19 -0
- package/dist/operations/services/railway-source-policy.js +66 -0
- package/dist/operations/services/repository-save-orchestrator.js +88 -19
- package/dist/operations/services/workspace-dependency-mode.js +4 -0
- package/dist/platform/desired-state.js +3 -3
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/providers/railway-iac.d.ts +2 -0
- package/dist/reconcile/providers/railway-iac.js +31 -3
- package/dist/reconcile/providers/release-private.d.ts +10 -0
- package/dist/reconcile/providers/release-private.js +45 -1
- package/dist/scenes/builtin-plugins.js +36 -5
- package/dist/scenes/device-matrix.js +2 -0
- package/dist/scenes/environment.js +1 -1
- package/dist/scenes/runner.js +28 -16
- package/dist/scenes/schema.js +31 -2
- package/dist/scenes/types.d.ts +25 -2
- package/dist/scenes/visual-audit-fixtures.js +9 -3
- package/dist/workflow/operations.d.ts +27 -92
- package/dist/workflow/operations.js +252 -144
- package/dist/workflow/runs.d.ts +1 -0
- package/dist/workflow/runs.js +57 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +4 -1
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, resolve, relative } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -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,
|
|
@@ -731,18 +731,28 @@ function syncDirectGitDependencyLockfileEntries(node, options, references) {
|
|
|
731
731
|
let changed = false;
|
|
732
732
|
for (const reference of references) {
|
|
733
733
|
const manifestSpec = reference.manifestSpec ?? reference.spec;
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
dependencies[reference.packageName] = manifestSpec;
|
|
740
|
-
changed = true;
|
|
734
|
+
const visitDependencyMaps = (value) => {
|
|
735
|
+
if (!value || typeof value !== "object") return;
|
|
736
|
+
if (Array.isArray(value)) {
|
|
737
|
+
for (const item of value) visitDependencyMaps(item);
|
|
738
|
+
return;
|
|
741
739
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
740
|
+
const record = value;
|
|
741
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
742
|
+
const dependencies = record[field];
|
|
743
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
|
|
744
|
+
const dependencyMap = dependencies;
|
|
745
|
+
const current = dependencyMap[reference.packageName];
|
|
746
|
+
if (typeof current === "string" && /(?:git|github:|#[0-9a-f]{7,40}$)/iu.test(current) && current !== manifestSpec) {
|
|
747
|
+
dependencyMap[reference.packageName] = manifestSpec;
|
|
748
|
+
changed = true;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (const nested of Object.values(record)) visitDependencyMaps(nested);
|
|
752
|
+
};
|
|
753
|
+
visitDependencyMaps(lockfile);
|
|
754
|
+
for (const [entryKey, entry] of Object.entries(packageEntries)) {
|
|
755
|
+
if (entryKey !== `node_modules/${reference.packageName}` && !entryKey.endsWith(`/node_modules/${reference.packageName}`)) continue;
|
|
746
756
|
const nextResolved = normalizeGitRemoteForDependency(reference.remoteUrl ?? "", "ssh");
|
|
747
757
|
const resolved = nextResolved ? `${nextResolved}#${manifestSpec.slice(manifestSpec.lastIndexOf("#") + 1)}` : manifestSpec;
|
|
748
758
|
if (entry.resolved !== resolved) {
|
|
@@ -764,6 +774,51 @@ function syncDirectGitDependencyLockfileEntries(node, options, references) {
|
|
|
764
774
|
emitProgress(options, node, "lockfile", "Synchronized direct internal Git dependency lockfile entries without npm git preparation.");
|
|
765
775
|
return true;
|
|
766
776
|
}
|
|
777
|
+
function validateStandaloneGitDependencyLockfile(node, options) {
|
|
778
|
+
const lockfilePath = resolve(node.path, "package-lock.json");
|
|
779
|
+
const lockfileExists = existsSync(lockfilePath);
|
|
780
|
+
const validateArgs = [
|
|
781
|
+
"ci",
|
|
782
|
+
"--package-lock-only",
|
|
783
|
+
"--ignore-scripts",
|
|
784
|
+
"--workspaces=false",
|
|
785
|
+
"--no-audit",
|
|
786
|
+
"--no-fund"
|
|
787
|
+
];
|
|
788
|
+
try {
|
|
789
|
+
if (!lockfileExists) throw new Error("standalone lockfile missing");
|
|
790
|
+
runCapturedCommand(node, options, "lockfile", "npm", validateArgs, { timeoutMs: 5 * 6e4 });
|
|
791
|
+
} catch (validationError) {
|
|
792
|
+
const previousLockfile = lockfileExists ? readFileSync(lockfilePath, "utf8") : null;
|
|
793
|
+
const isolatedRoot = mkdtempSync(resolve(tmpdir(), "treeseed-lockfile-"));
|
|
794
|
+
try {
|
|
795
|
+
copyFileSync(resolve(node.path, "package.json"), resolve(isolatedRoot, "package.json"));
|
|
796
|
+
runCapturedCommand(node, options, "lockfile", "npm", [
|
|
797
|
+
"install",
|
|
798
|
+
"--package-lock-only",
|
|
799
|
+
"--ignore-scripts",
|
|
800
|
+
"--workspaces=false",
|
|
801
|
+
"--no-audit",
|
|
802
|
+
"--no-fund"
|
|
803
|
+
], {
|
|
804
|
+
cwd: isolatedRoot,
|
|
805
|
+
timeoutMs: 15 * 6e4
|
|
806
|
+
});
|
|
807
|
+
runCapturedCommand(node, options, "lockfile", "npm", validateArgs, {
|
|
808
|
+
cwd: isolatedRoot,
|
|
809
|
+
timeoutMs: 5 * 6e4
|
|
810
|
+
});
|
|
811
|
+
copyFileSync(resolve(isolatedRoot, "package-lock.json"), lockfilePath);
|
|
812
|
+
} catch (regenerationError) {
|
|
813
|
+
if (previousLockfile !== null) writeFileSync(lockfilePath, previousLockfile, "utf8");
|
|
814
|
+
throw regenerationError instanceof Error ? regenerationError : validationError;
|
|
815
|
+
} finally {
|
|
816
|
+
rmSync(isolatedRoot, { recursive: true, force: true });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
emitProgress(options, node, "lockfile", "Validated the standalone lockfile for exact internal Git refs.");
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
767
822
|
function planPackageVersion(node, options) {
|
|
768
823
|
if (!node.packageJson || !node.packageJsonPath) return null;
|
|
769
824
|
const current = String(node.packageJson.version ?? "0.0.0");
|
|
@@ -777,7 +832,7 @@ function applyPackageVersion(node, version) {
|
|
|
777
832
|
return true;
|
|
778
833
|
}
|
|
779
834
|
function shouldSkipNetworkInstall() {
|
|
780
|
-
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE
|
|
835
|
+
return process.env.TREESEED_SAVE_NPM_INSTALL_MODE !== "allow";
|
|
781
836
|
}
|
|
782
837
|
function shouldSkipGitDependencySmoke(options) {
|
|
783
838
|
return shouldSkipNetworkInstall() || process.env.TREESEED_GIT_DEPENDENCY_SMOKE === "skip" || options?.verifyMode === "skip";
|
|
@@ -827,7 +882,7 @@ function syncRootWorkspaceLockfileMetadata(node, options) {
|
|
|
827
882
|
const mergedDeps = { ...currentDeps };
|
|
828
883
|
let fieldChanged = false;
|
|
829
884
|
for (const [dependencyName, dependencySpec] of Object.entries(nextValue)) {
|
|
830
|
-
if (mergedDeps[dependencyName]
|
|
885
|
+
if (mergedDeps[dependencyName] === dependencySpec) continue;
|
|
831
886
|
mergedDeps[dependencyName] = dependencySpec;
|
|
832
887
|
fieldChanged = true;
|
|
833
888
|
}
|
|
@@ -948,6 +1003,11 @@ function lockfileValidationCommand(node, options) {
|
|
|
948
1003
|
const args = rootWorkspaceInstall ? ["ci", "--ignore-scripts", "--plan"] : ["ci", "--ignore-scripts", "--plan", "--workspaces=false"];
|
|
949
1004
|
return { command: "npm", args };
|
|
950
1005
|
}
|
|
1006
|
+
function lockfileValidationTimeoutMs(node, options) {
|
|
1007
|
+
const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
|
|
1008
|
+
const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
|
|
1009
|
+
return rootWorkspaceInstall ? 18e5 : 6e5;
|
|
1010
|
+
}
|
|
951
1011
|
async function validateRepositoryLockfile(node, options) {
|
|
952
1012
|
if (!hasNpmLockfile(node.path)) {
|
|
953
1013
|
return { status: "skipped", command: null, issues: [], error: "no npm lockfile" };
|
|
@@ -973,7 +1033,7 @@ async function validateRepositoryLockfile(node, options) {
|
|
|
973
1033
|
return { status: "skipped", command: commandText, issues: [], error: "disabled" };
|
|
974
1034
|
}
|
|
975
1035
|
try {
|
|
976
|
-
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs:
|
|
1036
|
+
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: lockfileValidationTimeoutMs(node, options), emitOutputOnSuccess: false });
|
|
977
1037
|
const packageCount = npmLockfilePackageCount(node.path);
|
|
978
1038
|
const countText = packageCount === null ? "package-lock entries" : `${packageCount} package${packageCount === 1 ? "" : "s"}`;
|
|
979
1039
|
emitProgress(options, node, "lockfile", `Lockfile validation passed: ${countText} checked, 0 issues.`);
|
|
@@ -1124,7 +1184,8 @@ function pullRebaseFromOrigin(node, options, branch) {
|
|
|
1124
1184
|
};
|
|
1125
1185
|
}
|
|
1126
1186
|
try {
|
|
1127
|
-
runCapturedCommand(node, options, "rebase", "git", ["
|
|
1187
|
+
runCapturedCommand(node, options, "rebase", "git", ["fetch", "origin", `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1188
|
+
runCapturedCommand(node, options, "rebase", "git", ["rebase", `refs/remotes/origin/${branch}`]);
|
|
1128
1189
|
return {
|
|
1129
1190
|
remoteBranchExisted: true,
|
|
1130
1191
|
pulledRebase: true
|
|
@@ -1161,7 +1222,7 @@ function pushCurrentBranch(node, options, branch, tagName) {
|
|
|
1161
1222
|
async function finishRepositorySavePublish(node, options, state, report, input) {
|
|
1162
1223
|
const reference = input.reference ?? null;
|
|
1163
1224
|
const tagName = input.tagName ?? reference?.tagName ?? null;
|
|
1164
|
-
const shouldDeferPush = options.deferPushUntilVerified === true
|
|
1225
|
+
const shouldDeferPush = options.deferPushUntilVerified === true;
|
|
1165
1226
|
if (shouldDeferPush) {
|
|
1166
1227
|
state.deferredPushes.push({
|
|
1167
1228
|
node,
|
|
@@ -1705,8 +1766,15 @@ async function saveOneRepository(node, options, state) {
|
|
|
1705
1766
|
ensureWritableRemote(node, options);
|
|
1706
1767
|
const dependencyUpdates = isRootWorkspaceRepository(node, options) ? [] : updateDependencyReferences(node, state.finalizedReferences);
|
|
1707
1768
|
const dependencyChanged = dependencyUpdates.length > 0;
|
|
1708
|
-
const
|
|
1769
|
+
const directDependencyNames = new Set(dependencyFields(node.packageJson ?? {}).flatMap((field) => {
|
|
1770
|
+
const value = node.packageJson?.[field];
|
|
1771
|
+
return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
1772
|
+
}));
|
|
1773
|
+
const gitDependencyRefreshReferences = [...state.finalizedReferences.values()].filter((reference) => reference.mode === "dev-git-commit" && directDependencyNames.has(reference.packageName));
|
|
1709
1774
|
const lockfileGitDependenciesSynced = syncDirectGitDependencyLockfileEntries(node, options, gitDependencyRefreshReferences);
|
|
1775
|
+
if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json")))) {
|
|
1776
|
+
validateStandaloneGitDependencyLockfile(node, options);
|
|
1777
|
+
}
|
|
1710
1778
|
const gitDependencyRefreshSpecs = lockfileGitDependenciesSynced ? [] : gitDependencyRefreshReferences.map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
|
|
1711
1779
|
const submodulePointers = collectSubmodulePointerChanges(node, state.finalizedCommits);
|
|
1712
1780
|
const submodulesChanged = submodulePointers.length > 0;
|
|
@@ -1828,6 +1896,7 @@ async function saveOneRepository(node, options, state) {
|
|
|
1828
1896
|
return report;
|
|
1829
1897
|
}
|
|
1830
1898
|
report.committed = true;
|
|
1899
|
+
report.commitSha = headCommit(node.path);
|
|
1831
1900
|
const rebase = pullRebaseFromOrigin(node, options, branch);
|
|
1832
1901
|
const verifyMode = options.verifyMode ?? "action-first";
|
|
1833
1902
|
if (node.kind === "project" && node.path === options.root && Array.isArray(node.packageJson?.workspaces)) {
|
|
@@ -274,6 +274,10 @@ function removeLinkCandidate(link, managedLinks) {
|
|
|
274
274
|
unlinkSync(link.linkPath);
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
277
|
+
if (managed) {
|
|
278
|
+
rmSync(link.linkPath, { recursive: true, force: true });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
277
281
|
if (isInstalledTreeseedPackage(link.linkPath, link.packageName)) {
|
|
278
282
|
rmSync(link.linkPath, { recursive: true, force: true });
|
|
279
283
|
return true;
|
|
@@ -62,6 +62,7 @@ function environmentFromTarget(target) {
|
|
|
62
62
|
return "staging";
|
|
63
63
|
}
|
|
64
64
|
function packageReleaseCapability(adapter) {
|
|
65
|
+
if (!adapter.capabilities.publish) return adapter.releaseChecks.length > 0 ? "deploy-only" : "none";
|
|
65
66
|
if (adapter.artifacts.some((artifact) => artifact.provider === "docker")) return "image";
|
|
66
67
|
if (adapter.artifacts.some((artifact) => artifact.provider === "npm")) return "npm";
|
|
67
68
|
if (adapter.releaseChecks.length > 0) return "deploy-only";
|
|
@@ -652,9 +653,8 @@ function localDevelopmentResources(tenantRoot, environment, localContent) {
|
|
|
652
653
|
dependencies: [composeId],
|
|
653
654
|
spec: {
|
|
654
655
|
mode: "local",
|
|
655
|
-
roles: ["
|
|
656
|
-
volumePolicy: "shared-local"
|
|
657
|
-
healthEndpoint: "http://127.0.0.1:4783/healthz"
|
|
656
|
+
roles: ["manager", "runner"],
|
|
657
|
+
volumePolicy: "shared-local"
|
|
658
658
|
},
|
|
659
659
|
source: { type: "package-adapter", id: "@treeseed/agent" }
|
|
660
660
|
},
|
|
@@ -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>;
|