@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
package/dist/hosting/graph.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
6
6
|
import { createPersistentDeployTarget } from "../operations/services/deploy.js";
|
|
7
7
|
import { collectTreeseedConfigSeedValues, resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
|
|
8
8
|
import { classifyTreeseedGitMode, runTreeseedGitText } from "../operations/services/git-runner.js";
|
|
9
|
+
import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../operations/services/railway-source-policy.js";
|
|
9
10
|
import { createTreeseedCanonicalReconcileReport } from "../reconcile/index.js";
|
|
10
11
|
import { reconcileTreeseedTarget } from "../reconcile/index.js";
|
|
11
12
|
import { discoverTreeseedApplications, findTreeseedApplication } from "./apps.js";
|
|
@@ -64,12 +65,15 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
64
65
|
const railway = config.publicTreeDxFederation?.railway ?? {};
|
|
65
66
|
const configuredSource = railway.source && typeof railway.source === "object" && !Array.isArray(railway.source) ? railway.source : {};
|
|
66
67
|
const configuredMode = typeof railway.sourceMode === "string" ? railway.sourceMode : null;
|
|
68
|
+
if (input.environment === "staging" && configuredMode === "image") {
|
|
69
|
+
throw new Error("public-treedx-node-01: API Railway staging services must use GitHub Dockerfile source builds (configured sourceMode image is not allowed).");
|
|
70
|
+
}
|
|
67
71
|
const treeDxRoot = resolvePublicTreeDxRoot(input);
|
|
68
72
|
const repository = typeof railway.sourceRepo === "string" ? railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(treeDxRoot) ?? "treeseed-ai/treedx";
|
|
69
73
|
const sourceMode = configuredMode === "git" || configuredMode === "image" ? configuredMode : input.environment === "staging" ? "git" : "image";
|
|
70
74
|
if (sourceMode !== "git") {
|
|
71
75
|
const imageRef = typeof launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF === "string" && launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() ? launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() : null;
|
|
72
|
-
|
|
76
|
+
const policy2 = {
|
|
73
77
|
sourceMode: "image",
|
|
74
78
|
sourceRepo: null,
|
|
75
79
|
sourceBranch: null,
|
|
@@ -79,8 +83,10 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
79
83
|
imageRef,
|
|
80
84
|
imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
|
|
81
85
|
};
|
|
86
|
+
assertApiRailwaySourcePolicy(input.environment, { key: "public-treedx-node-01", serviceName: "public-treedx-node-01", ...policy2 });
|
|
87
|
+
return policy2;
|
|
82
88
|
}
|
|
83
|
-
|
|
89
|
+
const policy = {
|
|
84
90
|
sourceMode: "git",
|
|
85
91
|
sourceRepo: repository,
|
|
86
92
|
sourceBranch: typeof railway.sourceBranch === "string" ? railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : "staging",
|
|
@@ -89,6 +95,13 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
|
|
|
89
95
|
image: null,
|
|
90
96
|
imageTagRef: null
|
|
91
97
|
};
|
|
98
|
+
assertApiRailwaySourcePolicy(input.environment, {
|
|
99
|
+
key: "public-treedx-node-01",
|
|
100
|
+
serviceName: "public-treedx-node-01",
|
|
101
|
+
dockerfilePath: railway.dockerfilePath ?? "/Dockerfile",
|
|
102
|
+
...policy
|
|
103
|
+
});
|
|
104
|
+
return policy;
|
|
92
105
|
}
|
|
93
106
|
function serviceKeyPlacement(serviceKey) {
|
|
94
107
|
if (serviceKey === "api") return "api";
|
|
@@ -161,11 +174,16 @@ function resolveRailwayServiceSourceRoot(input, serviceKey, service) {
|
|
|
161
174
|
function railwaySourcePolicy(input, serviceKey, service, imageRef) {
|
|
162
175
|
const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
|
|
163
176
|
const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
|
|
164
|
-
const
|
|
177
|
+
const serviceName = service.railway?.serviceName ?? null;
|
|
178
|
+
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 });
|
|
179
|
+
const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
|
|
165
180
|
const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
|
|
181
|
+
if (input.environment === "staging" && isApiRailwaySourcePolicyService({ key: serviceKey, serviceName }) && (configuredMode === "image" || service.railway?.imageRef)) {
|
|
182
|
+
throw new Error(`${serviceName ?? serviceKey}: API Railway staging services must use GitHub Dockerfile source builds (configured image source is not allowed).`);
|
|
183
|
+
}
|
|
166
184
|
const mode = input.environment === "prod" ? "image" : input.environment === "staging" && apiPackageSourceEligible ? "git" : configuredMode === "git" || configuredMode === "image" ? configuredMode : imageRef ? "image" : "git";
|
|
167
185
|
if (mode !== "git") {
|
|
168
|
-
|
|
186
|
+
const policy2 = {
|
|
169
187
|
sourceMode: "image",
|
|
170
188
|
sourceRepo: null,
|
|
171
189
|
sourceBranch: null,
|
|
@@ -173,8 +191,17 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
|
|
|
173
191
|
sourceRootDirectory: null,
|
|
174
192
|
imageRef
|
|
175
193
|
};
|
|
194
|
+
assertApiRailwaySourcePolicy(input.environment, {
|
|
195
|
+
key: serviceKey,
|
|
196
|
+
serviceName,
|
|
197
|
+
dockerfilePath: null,
|
|
198
|
+
buildCommand: null,
|
|
199
|
+
startCommand: null,
|
|
200
|
+
...policy2
|
|
201
|
+
});
|
|
202
|
+
return policy2;
|
|
176
203
|
}
|
|
177
|
-
|
|
204
|
+
const policy = {
|
|
178
205
|
sourceMode: "git",
|
|
179
206
|
sourceRepo: repository,
|
|
180
207
|
sourceBranch: typeof service.railway?.sourceBranch === "string" ? service.railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : input.environment === "staging" ? "staging" : null,
|
|
@@ -182,6 +209,13 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
|
|
|
182
209
|
sourceRootDirectory: typeof service.railway?.sourceRootDirectory === "string" ? service.railway.sourceRootDirectory : typeof configuredSource.rootDirectory === "string" ? configuredSource.rootDirectory : ".",
|
|
183
210
|
imageRef: null
|
|
184
211
|
};
|
|
212
|
+
assertApiRailwaySourcePolicy(input.environment, {
|
|
213
|
+
key: serviceKey,
|
|
214
|
+
serviceName,
|
|
215
|
+
dockerfilePath,
|
|
216
|
+
...policy
|
|
217
|
+
});
|
|
218
|
+
return policy;
|
|
185
219
|
}
|
|
186
220
|
function collectPluginHostingContributions(input) {
|
|
187
221
|
const plugins = loadTreeseedPlugins(input.deployConfig);
|
|
@@ -358,7 +392,7 @@ function buildProfileFromDeployConfig(input) {
|
|
|
358
392
|
sourceBranch: sourcePolicy.sourceBranch,
|
|
359
393
|
sourceCommit: sourcePolicy.sourceCommit,
|
|
360
394
|
sourceRootDirectory: sourcePolicy.sourceRootDirectory,
|
|
361
|
-
dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? null : null,
|
|
395
|
+
dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
|
|
362
396
|
buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
|
|
363
397
|
startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
|
|
364
398
|
healthcheckPath: service.railway?.healthcheckPath ?? null,
|
|
@@ -123,6 +123,16 @@ export declare function checkoutTaskBranchFromStaging(cwd: any, branchName: any,
|
|
|
123
123
|
resumed: boolean;
|
|
124
124
|
remoteBranch: boolean;
|
|
125
125
|
};
|
|
126
|
+
export declare function checkoutNewTaskBranchWithChanges(cwd: string, branchName: string, { pushIfCreated }?: {
|
|
127
|
+
pushIfCreated?: boolean | undefined;
|
|
128
|
+
}): {
|
|
129
|
+
repoDir: string;
|
|
130
|
+
branchName: string;
|
|
131
|
+
baseBranch: string;
|
|
132
|
+
created: boolean;
|
|
133
|
+
resumed: boolean;
|
|
134
|
+
remoteBranch: boolean;
|
|
135
|
+
};
|
|
126
136
|
export declare function syncBranchWithOrigin(repoDir: any, branchName: any): void;
|
|
127
137
|
export declare function checkoutDetachedOriginBranch(repoDir: any, branchName: any): void;
|
|
128
138
|
export declare function pushHeadToBranch(repoDir: any, branchName: any): void;
|
|
@@ -201,13 +211,18 @@ export declare function listTaskBranches(repoDir: any): {
|
|
|
201
211
|
remote: boolean;
|
|
202
212
|
current: boolean;
|
|
203
213
|
}[];
|
|
204
|
-
export declare function
|
|
214
|
+
export declare function shouldRetryFailedStagingAutomation(status: string | null, conclusion: string | null): boolean;
|
|
215
|
+
export declare function waitForStagingAutomation(repoDir: any, env?: NodeJS.ProcessEnv): {
|
|
205
216
|
status: string;
|
|
206
217
|
reason: string;
|
|
207
218
|
branch?: undefined;
|
|
219
|
+
headSha?: undefined;
|
|
220
|
+
runId?: undefined;
|
|
208
221
|
} | {
|
|
209
222
|
status: string;
|
|
210
223
|
branch: string;
|
|
224
|
+
headSha: string;
|
|
225
|
+
runId: number;
|
|
211
226
|
reason?: undefined;
|
|
212
227
|
};
|
|
213
228
|
export declare function prepareReleaseBranches(cwd?: any): string;
|
|
@@ -437,6 +437,25 @@ function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true
|
|
|
437
437
|
remoteBranch: pushIfCreated
|
|
438
438
|
};
|
|
439
439
|
}
|
|
440
|
+
function checkoutNewTaskBranchWithChanges(cwd, branchName, { pushIfCreated = false } = {}) {
|
|
441
|
+
const repoDir = repoRoot(cwd);
|
|
442
|
+
if (currentBranch(repoDir) !== STAGING_BRANCH) {
|
|
443
|
+
throw new Error(`Dirty change adoption requires ${repoDir} to be on ${STAGING_BRANCH}.`);
|
|
444
|
+
}
|
|
445
|
+
if (branchExists(repoDir, branchName) || remoteBranchExists(repoDir, branchName)) {
|
|
446
|
+
throw new Error(`Dirty change adoption requires a new branch; ${branchName} already exists.`);
|
|
447
|
+
}
|
|
448
|
+
runGit(["checkout", "-b", branchName], { cwd: repoDir });
|
|
449
|
+
if (pushIfCreated) pushBranch(repoDir, branchName, { setUpstream: true });
|
|
450
|
+
return {
|
|
451
|
+
repoDir,
|
|
452
|
+
branchName,
|
|
453
|
+
baseBranch: STAGING_BRANCH,
|
|
454
|
+
created: true,
|
|
455
|
+
resumed: false,
|
|
456
|
+
remoteBranch: pushIfCreated
|
|
457
|
+
};
|
|
458
|
+
}
|
|
440
459
|
function syncBranchWithOrigin(repoDir, branchName) {
|
|
441
460
|
fetchOrigin(repoDir);
|
|
442
461
|
if (!branchExists(repoDir, branchName) && remoteBranchExists(repoDir, branchName)) {
|
|
@@ -637,20 +656,59 @@ function listTaskBranches(repoDir) {
|
|
|
637
656
|
};
|
|
638
657
|
});
|
|
639
658
|
}
|
|
640
|
-
function
|
|
659
|
+
function shouldRetryFailedStagingAutomation(status, conclusion) {
|
|
660
|
+
return status === "completed" && Boolean(conclusion) && conclusion !== "success";
|
|
661
|
+
}
|
|
662
|
+
function waitForStagingAutomation(repoDir, env = process.env) {
|
|
641
663
|
if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
642
664
|
return { status: "skipped", reason: "disabled" };
|
|
643
665
|
}
|
|
644
666
|
try {
|
|
645
|
-
const gh = resolveTreeseedToolBinary("gh");
|
|
667
|
+
const gh = resolveTreeseedToolBinary("gh", { env });
|
|
646
668
|
if (!gh) {
|
|
647
669
|
throw new Error("GitHub CLI `gh` is unavailable.");
|
|
648
670
|
}
|
|
649
|
-
|
|
671
|
+
const headSha = remoteHeadCommit(repoDir, STAGING_BRANCH);
|
|
672
|
+
let runId = null;
|
|
673
|
+
let runStatus = null;
|
|
674
|
+
let runConclusion = null;
|
|
675
|
+
for (let attempt = 0; attempt < 120 && runId == null; attempt += 1) {
|
|
676
|
+
const output = run(gh, [
|
|
677
|
+
"run",
|
|
678
|
+
"list",
|
|
679
|
+
"--workflow",
|
|
680
|
+
"deploy.yml",
|
|
681
|
+
"--branch",
|
|
682
|
+
STAGING_BRANCH,
|
|
683
|
+
"--commit",
|
|
684
|
+
headSha,
|
|
685
|
+
"--limit",
|
|
686
|
+
"1",
|
|
687
|
+
"--json",
|
|
688
|
+
"databaseId,status,conclusion"
|
|
689
|
+
], {
|
|
690
|
+
cwd: repoDir,
|
|
691
|
+
env: createTreeseedManagedToolEnv(env),
|
|
692
|
+
capture: true
|
|
693
|
+
});
|
|
694
|
+
const rows = JSON.parse(output || "[]");
|
|
695
|
+
runId = typeof rows[0]?.databaseId === "number" ? rows[0].databaseId : null;
|
|
696
|
+
runStatus = typeof rows[0]?.status === "string" ? rows[0].status : null;
|
|
697
|
+
runConclusion = typeof rows[0]?.conclusion === "string" ? rows[0].conclusion : null;
|
|
698
|
+
if (runId == null) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5e3);
|
|
699
|
+
}
|
|
700
|
+
if (runId == null) throw new Error(`No deploy.yml run appeared for ${headSha}.`);
|
|
701
|
+
if (shouldRetryFailedStagingAutomation(runStatus, runConclusion)) {
|
|
702
|
+
run(gh, ["run", "rerun", String(runId), "--failed"], {
|
|
703
|
+
cwd: repoDir,
|
|
704
|
+
env: createTreeseedManagedToolEnv(env)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
run(gh, ["run", "watch", String(runId), "--exit-status"], {
|
|
650
708
|
cwd: repoDir,
|
|
651
|
-
env: createTreeseedManagedToolEnv(
|
|
709
|
+
env: createTreeseedManagedToolEnv(env)
|
|
652
710
|
});
|
|
653
|
-
return { status: "completed", branch: STAGING_BRANCH };
|
|
711
|
+
return { status: "completed", branch: STAGING_BRANCH, headSha, runId };
|
|
654
712
|
} catch (error) {
|
|
655
713
|
throw new Error([
|
|
656
714
|
"Treeseed stage could not confirm the staging deploy/checks completed.",
|
|
@@ -710,6 +768,7 @@ export {
|
|
|
710
768
|
branchExists,
|
|
711
769
|
checkoutBranch,
|
|
712
770
|
checkoutDetachedOriginBranch,
|
|
771
|
+
checkoutNewTaskBranchWithChanges,
|
|
713
772
|
checkoutTaskBranchFromStaging,
|
|
714
773
|
createDeprecatedTaskTag,
|
|
715
774
|
createFeatureBranchFromStaging,
|
|
@@ -736,6 +795,7 @@ export {
|
|
|
736
795
|
reattachDetachedHeadIfSafe,
|
|
737
796
|
remoteBranchExists,
|
|
738
797
|
remoteHeadCommit,
|
|
798
|
+
shouldRetryFailedStagingAutomation,
|
|
739
799
|
squashMergeBranchIntoStaging,
|
|
740
800
|
syncBranchWithOrigin,
|
|
741
801
|
taskTagSlug,
|
|
@@ -792,25 +792,6 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
792
792
|
monitorErrorStartedAt = null;
|
|
793
793
|
lastMonitorError = null;
|
|
794
794
|
const matchingRuns = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).filter((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
|
|
795
|
-
const failedMatch = matchingRuns.find((run) => run.status === "completed" && run.conclusion !== "success");
|
|
796
|
-
if (failedMatch?.id) {
|
|
797
|
-
const failedJobs = await listWorkflowJobsForProgress(client, owner, name, failedMatch.id);
|
|
798
|
-
emitProgress("completed", failedMatch, failedJobs);
|
|
799
|
-
return {
|
|
800
|
-
status: "completed",
|
|
801
|
-
repository: `${owner}/${name}`,
|
|
802
|
-
workflow,
|
|
803
|
-
runId: failedMatch.id,
|
|
804
|
-
headSha: failedMatch.headSha,
|
|
805
|
-
branch: failedMatch.headBranch,
|
|
806
|
-
createdAt: failedMatch.createdAt,
|
|
807
|
-
updatedAt: failedMatch.updatedAt,
|
|
808
|
-
conclusion: failedMatch.conclusion,
|
|
809
|
-
url: failedMatch.url,
|
|
810
|
-
jobs: failedJobs,
|
|
811
|
-
failedJobs: failedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
795
|
const match = matchingRuns[0];
|
|
815
796
|
if (!match?.id) {
|
|
816
797
|
emitProgress("waiting");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues } from "./config-runtime.js";
|
|
3
3
|
import { configuredRailwayServices } from "./railway-deploy.js";
|
|
4
|
+
import { isApiRailwaySourcePolicyService, isImmutableRailwayImageRef } from "./railway-source-policy.js";
|
|
4
5
|
import { loadTreeseedPlatformConfig } from "../../platform/config.js";
|
|
5
6
|
import { discoverTreeseedApplications } from "../../hosting/apps.js";
|
|
6
7
|
const RAILWAY_SECRET_KEYS_BY_SERVICE = {
|
|
@@ -221,6 +222,21 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
221
222
|
const serviceType = serviceTypeFor(service.key);
|
|
222
223
|
const observed = observedFor(options, service.serviceName);
|
|
223
224
|
const expectedRootDirectory = railwayServiceRootDirectory(tenantRoot, service);
|
|
225
|
+
const apiSourcePolicyService = isApiRailwaySourcePolicyService(service);
|
|
226
|
+
if (target === "prod" && apiSourcePolicyService && railwayServiceUsesImageSource(service)) {
|
|
227
|
+
checks.push(check({
|
|
228
|
+
id: `railway:${service.instanceKey}:image-ref-policy`,
|
|
229
|
+
provider: "railway",
|
|
230
|
+
serviceKey: service.key,
|
|
231
|
+
serviceType,
|
|
232
|
+
target,
|
|
233
|
+
description: `Railway ${service.serviceName} uses an immutable production image reference.`,
|
|
234
|
+
expected: { imageRef: "<released-image>:<immutable-tag>" },
|
|
235
|
+
observed: { imageRef: service.imageRef ?? null },
|
|
236
|
+
status: isImmutableRailwayImageRef(service.imageRef) ? "passed" : "failed",
|
|
237
|
+
issues: isImmutableRailwayImageRef(service.imageRef) ? [] : [`Production API Railway service ${service.serviceName} is missing an immutable image ref.`]
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
224
240
|
checks.push(check({
|
|
225
241
|
id: `railway:${service.instanceKey}:service`,
|
|
226
242
|
provider: "railway",
|
|
@@ -261,7 +277,7 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
261
277
|
const actualRepo = observed?.deploymentRepo ?? null;
|
|
262
278
|
const actualBranch = observed?.deploymentBranch ?? null;
|
|
263
279
|
const actualRootDirectory = observed?.deploymentRootDirectory ?? null;
|
|
264
|
-
const sourceUploadDeployment = target === "staging" && actualRepo === null && actualBranch === null && observed?.deploymentHealthy === true;
|
|
280
|
+
const sourceUploadDeployment = target === "staging" && !apiSourcePolicyService && actualRepo === null && actualBranch === null && observed?.deploymentHealthy === true;
|
|
265
281
|
checks.push(check({
|
|
266
282
|
id: `railway:${service.instanceKey}:deployment-repo`,
|
|
267
283
|
provider: "railway",
|
|
@@ -12,6 +12,10 @@ export interface TreeseedLiveHostedServiceCheckOptions {
|
|
|
12
12
|
attempts: number;
|
|
13
13
|
intervalMs: number;
|
|
14
14
|
};
|
|
15
|
+
httpRetry?: {
|
|
16
|
+
attempts: number;
|
|
17
|
+
intervalMs: number;
|
|
18
|
+
};
|
|
15
19
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
16
20
|
fetchImpl?: typeof fetch;
|
|
17
21
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "./hosted-service-checks.js";
|
|
20
20
|
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
21
21
|
const DEFAULT_RETRY_INTERVAL_MS = 1500;
|
|
22
|
-
const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS =
|
|
22
|
+
const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 120;
|
|
23
23
|
const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_INTERVAL_MS = 5e3;
|
|
24
24
|
function sleep(ms) {
|
|
25
25
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -46,10 +46,11 @@ function pagesBranchName(config, target) {
|
|
|
46
46
|
return typeof pages[key] === "string" && pages[key].trim() ? pages[key].trim() : fallback;
|
|
47
47
|
}
|
|
48
48
|
async function observeHttp(url, options) {
|
|
49
|
-
const attempts = Math.max(1, Math.floor(options.retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS));
|
|
50
|
-
const intervalMs = Math.max(0, Math.floor(options.retry?.intervalMs ?? DEFAULT_RETRY_INTERVAL_MS));
|
|
49
|
+
const attempts = Math.max(1, Math.floor(options.httpRetry?.attempts ?? options.retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS));
|
|
50
|
+
const intervalMs = Math.max(0, Math.floor(options.httpRetry?.intervalMs ?? options.retry?.intervalMs ?? DEFAULT_RETRY_INTERVAL_MS));
|
|
51
51
|
const timeoutMs = Math.max(1e3, Math.floor(options.timeoutMs ?? 1e4));
|
|
52
52
|
let lastError = "";
|
|
53
|
+
let lastResponse = null;
|
|
53
54
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
54
55
|
const controller = new AbortController();
|
|
55
56
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -60,14 +61,16 @@ async function observeHttp(url, options) {
|
|
|
60
61
|
signal: controller.signal
|
|
61
62
|
});
|
|
62
63
|
clearTimeout(timeout);
|
|
63
|
-
|
|
64
|
+
lastResponse = { status: response.status, ok: response.ok };
|
|
65
|
+
if (response.ok) return lastResponse;
|
|
64
66
|
} catch (error) {
|
|
65
67
|
clearTimeout(timeout);
|
|
66
68
|
lastError = error instanceof Error ? error.message : String(error);
|
|
67
69
|
if (attempt + 1 < attempts) await sleep(intervalMs);
|
|
68
70
|
}
|
|
71
|
+
if (attempt + 1 < attempts) await sleep(intervalMs);
|
|
69
72
|
}
|
|
70
|
-
return { ok: false, error: lastError || "HTTP request failed." };
|
|
73
|
+
return lastResponse ?? { ok: false, error: lastError || "HTTP request failed." };
|
|
71
74
|
}
|
|
72
75
|
async function inspectRailwayServiceDeploymentHealthWithRetry(input) {
|
|
73
76
|
const attempts = Math.max(1, Math.floor(input.options.retry?.attempts ?? DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS));
|
|
@@ -44,15 +44,11 @@ function runTreeseedLocalCleanup(input) {
|
|
|
44
44
|
".treeseed/cache",
|
|
45
45
|
".treeseed/scenes/runs",
|
|
46
46
|
".treeseed/scenes/matrix",
|
|
47
|
-
".treeseed/scenes/render"
|
|
48
|
-
".treeseed/guarantees/runs",
|
|
49
|
-
".treeseed/guarantees/release",
|
|
50
|
-
".treeseed/generated/release-evidence",
|
|
51
|
-
".treeseed/release/evidence"
|
|
47
|
+
".treeseed/scenes/render"
|
|
52
48
|
] : [".treeseed/tmp", ".treeseed/cache", ".treeseed/scenes/render"];
|
|
53
49
|
for (const target of directoryTargets) actions.push(removeDirectory(root, target));
|
|
54
|
-
if (input.docker
|
|
55
|
-
if (input.npmCache
|
|
50
|
+
if (input.docker === true && mode === "aggressive") actions.push(runCleanupCommand("docker-system-prune", "docker", ["docker", "system", "prune", "--all", "--volumes", "--force"], root));
|
|
51
|
+
if (input.npmCache === true) actions.push(runCleanupCommand("npm-cache-clean", "npm-cache", ["npm", "cache", "clean", "--force"], root));
|
|
56
52
|
const afterBytes = directoryBytes(join(root, ".treeseed"));
|
|
57
53
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
58
54
|
return { ok: actions.every((entry) => entry.status !== "failed"), mode, root, startedAt, completedAt, beforeBytes, afterBytes, reclaimedBytes: Math.max(0, beforeBytes - afterBytes), actions };
|
|
@@ -31,6 +31,13 @@ export type TreeseedPackageAdapter = {
|
|
|
31
31
|
role?: string | null;
|
|
32
32
|
architectures?: string[];
|
|
33
33
|
}>;
|
|
34
|
+
capabilities: {
|
|
35
|
+
save: boolean;
|
|
36
|
+
verify: boolean;
|
|
37
|
+
publish: boolean;
|
|
38
|
+
deploy: boolean;
|
|
39
|
+
localOnly: boolean;
|
|
40
|
+
};
|
|
34
41
|
releaseChecks: Array<{
|
|
35
42
|
kind: 'npm-pack-plan' | 'github-workflow' | 'docker-manifest';
|
|
36
43
|
name: string;
|
|
@@ -139,6 +146,13 @@ export declare function packageAdapterPlanSummary(root?: any): {
|
|
|
139
146
|
role?: string | null;
|
|
140
147
|
architectures?: string[];
|
|
141
148
|
}[];
|
|
149
|
+
capabilities: {
|
|
150
|
+
save: boolean;
|
|
151
|
+
verify: boolean;
|
|
152
|
+
publish: boolean;
|
|
153
|
+
deploy: boolean;
|
|
154
|
+
localOnly: boolean;
|
|
155
|
+
};
|
|
142
156
|
releaseChecks: {
|
|
143
157
|
kind: "npm-pack-plan" | "github-workflow" | "docker-manifest";
|
|
144
158
|
name: string;
|
|
@@ -110,12 +110,17 @@ function nodeTypeScriptAdapter(pkg) {
|
|
|
110
110
|
const dockerImages = stringRecord(manifest?.dockerImages);
|
|
111
111
|
const dockerImageReleaseWorkflow = stringValue(dockerImages.releaseWorkflow);
|
|
112
112
|
const dockerImageArchitectures = stringArray(dockerImages.architectures);
|
|
113
|
+
const releaseGateRecord = stringRecord(manifest?.releaseGate);
|
|
114
|
+
const hostedVerifyTimeoutSeconds = positiveIntegerValue(releaseGateRecord.timeoutSeconds) ?? positiveIntegerValue(manifest?.hostedVerifyTimeoutSeconds);
|
|
113
115
|
const publishTargetRaw = stringValue(manifest?.publishTarget);
|
|
114
116
|
const publishTarget = dockerArtifacts.length > 0 && publishTargetRaw === "docker" ? dockerArtifacts[0].name : publishTargetRaw ?? "npm";
|
|
115
|
-
const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(
|
|
117
|
+
const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(releaseGateRecord.workflow) ?? (existsSync(resolve(pkg.dir, ".github/workflows/deploy.yml")) ? "deploy.yml" : null);
|
|
116
118
|
const projectArchitecture = normalizeTreeseedPackageProjectArchitecture(manifest?.projectArchitecture, id);
|
|
117
119
|
const docsReadiness = docsSiteReadiness(pkg.dir, projectArchitecture);
|
|
118
120
|
const verifyLocal = typeof scripts["verify:local"] === "string" ? "verify:local" : typeof scripts.verify === "string" ? "verify" : typeof scripts["verify:action"] === "string" ? "verify:action" : null;
|
|
121
|
+
const capabilityRecord = stringRecord(manifest?.capabilities);
|
|
122
|
+
const localOnly = capabilityRecord.localOnly === true;
|
|
123
|
+
const packageType = stringValue(manifest?.type);
|
|
119
124
|
return {
|
|
120
125
|
id,
|
|
121
126
|
name,
|
|
@@ -140,6 +145,13 @@ function nodeTypeScriptAdapter(pkg) {
|
|
|
140
145
|
role: artifact.role,
|
|
141
146
|
architectures: artifact.architectures
|
|
142
147
|
})) : [{ provider: "npm", name: String(pkg.name) }],
|
|
148
|
+
capabilities: {
|
|
149
|
+
save: capabilityRecord.save !== false,
|
|
150
|
+
verify: capabilityRecord.verify !== false,
|
|
151
|
+
publish: capabilityRecord.publish !== false,
|
|
152
|
+
deploy: !localOnly && (capabilityRecord.deploy === true || packageType === "hosted-service" || packageType === "hosted-app"),
|
|
153
|
+
localOnly
|
|
154
|
+
},
|
|
143
155
|
releaseChecks: [
|
|
144
156
|
{ kind: "github-workflow", name: "publish workflow", detail: dockerImageReleaseWorkflow ? `.github/workflows/${dockerImageReleaseWorkflow}` : ".github/workflows/publish.yml" },
|
|
145
157
|
...dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({ kind: "docker-manifest", name: `${artifact.name} Docker image manifest`, detail: `${artifact.name}:<version>` })) : [{ kind: "npm-pack-plan", name: "npm pack", detail: "npm pack --plan" }]
|
|
@@ -153,7 +165,7 @@ function nodeTypeScriptAdapter(pkg) {
|
|
|
153
165
|
dockerImageArchitectures,
|
|
154
166
|
imageHosting: stringRecord(dockerImages.hosting)
|
|
155
167
|
} : {},
|
|
156
|
-
type:
|
|
168
|
+
type: packageType ?? null,
|
|
157
169
|
githubEnvironments: stringArray(manifest?.githubEnvironments),
|
|
158
170
|
requiredSecrets: stringArray(manifest?.requiredSecrets),
|
|
159
171
|
requiredVariables: stringArray(manifest?.requiredVariables),
|
|
@@ -168,6 +180,7 @@ function nodeTypeScriptAdapter(pkg) {
|
|
|
168
180
|
...hostedVerifyWorkflow ? {
|
|
169
181
|
hostedVerifyWorkflow: hostedVerifyWorkflow.startsWith(".github/workflows/") ? hostedVerifyWorkflow : `.github/workflows/${hostedVerifyWorkflow}`
|
|
170
182
|
} : {},
|
|
183
|
+
...hostedVerifyTimeoutSeconds ? { hostedVerifyTimeoutSeconds } : {},
|
|
171
184
|
scripts
|
|
172
185
|
}
|
|
173
186
|
};
|
|
@@ -189,6 +202,10 @@ function stringRecord(value) {
|
|
|
189
202
|
function stringValue(value) {
|
|
190
203
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
191
204
|
}
|
|
205
|
+
function positiveIntegerValue(value) {
|
|
206
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value.trim()) : NaN;
|
|
207
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
|
|
208
|
+
}
|
|
192
209
|
function stringArray(value) {
|
|
193
210
|
return Array.isArray(value) ? value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry)) : [];
|
|
194
211
|
}
|
|
@@ -225,10 +242,14 @@ function beamPackageAdapter(root, dir) {
|
|
|
225
242
|
const dockerImages = stringRecord(manifest?.dockerImages);
|
|
226
243
|
const dockerImageReleaseWorkflow = stringValue(dockerImages.releaseWorkflow);
|
|
227
244
|
const dockerImageArchitectures = stringArray(dockerImages.architectures);
|
|
245
|
+
const releaseGateRecord = stringRecord(manifest?.releaseGate);
|
|
246
|
+
const hostedVerifyTimeoutSeconds = positiveIntegerValue(releaseGateRecord.timeoutSeconds) ?? positiveIntegerValue(manifest?.hostedVerifyTimeoutSeconds);
|
|
228
247
|
const repository = stringValue(manifest?.repository) ?? (id === "treedx" ? "treeseed-ai/treedx" : null);
|
|
229
|
-
const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(
|
|
248
|
+
const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(releaseGateRecord.workflow) ?? (existsSync(resolve(dir, ".github/workflows/release-gate.yml")) ? "release-gate.yml" : null);
|
|
230
249
|
const projectArchitecture = normalizeTreeseedPackageProjectArchitecture(manifest?.projectArchitecture, id);
|
|
231
250
|
const docsReadiness = docsSiteReadiness(dir, projectArchitecture);
|
|
251
|
+
const capabilityRecord = stringRecord(manifest?.capabilities);
|
|
252
|
+
const localOnly = capabilityRecord.localOnly === true;
|
|
232
253
|
const verify = manifest?.verify && typeof manifest.verify === "object" && !Array.isArray(manifest.verify) ? manifest.verify : {};
|
|
233
254
|
const fast = verify.fast ?? (existsSync(resolve(dir, "scripts/test-treedx-fast.sh")) ? "scripts/test-treedx-fast.sh" : null);
|
|
234
255
|
const local = verify.local ?? (existsSync(resolve(dir, "scripts/test-all.sh")) ? "scripts/test-all.sh" : null);
|
|
@@ -269,6 +290,13 @@ function beamPackageAdapter(root, dir) {
|
|
|
269
290
|
role: id,
|
|
270
291
|
architectures: dockerImageArchitectures
|
|
271
292
|
}] : [],
|
|
293
|
+
capabilities: {
|
|
294
|
+
save: capabilityRecord.save !== false,
|
|
295
|
+
verify: capabilityRecord.verify !== false,
|
|
296
|
+
publish: capabilityRecord.publish !== false,
|
|
297
|
+
deploy: !localOnly && capabilityRecord.deploy !== false,
|
|
298
|
+
localOnly
|
|
299
|
+
},
|
|
272
300
|
releaseChecks: dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({ kind: "docker-manifest", name: `${artifact.name} Docker image manifest`, detail: `${artifact.name}:${version ?? "<version>"}` })) : image ? [{ kind: "docker-manifest", name: "Docker image manifest", detail: `${image}:${version ?? "<version>"}` }] : [],
|
|
273
301
|
metadata: {
|
|
274
302
|
hasCargo: existsSync(resolve(dir, "Cargo.toml")),
|
|
@@ -282,6 +310,7 @@ function beamPackageAdapter(root, dir) {
|
|
|
282
310
|
...hostedVerifyWorkflow ? {
|
|
283
311
|
hostedVerifyWorkflow: hostedVerifyWorkflow.startsWith(".github/workflows/") ? hostedVerifyWorkflow : `.github/workflows/${hostedVerifyWorkflow}`
|
|
284
312
|
} : {},
|
|
313
|
+
...hostedVerifyTimeoutSeconds ? { hostedVerifyTimeoutSeconds } : {},
|
|
285
314
|
type: stringValue(manifest?.type) ?? null,
|
|
286
315
|
githubEnvironments: stringArray(manifest?.githubEnvironments),
|
|
287
316
|
requiredSecrets: stringArray(manifest?.requiredSecrets),
|
|
@@ -456,6 +485,7 @@ function packageAdapterPlanSummary(root = workspaceRoot()) {
|
|
|
456
485
|
command ? `${command.command} ${command.args.join(" ")}` : null
|
|
457
486
|
])),
|
|
458
487
|
artifacts: adapter.artifacts,
|
|
488
|
+
capabilities: adapter.capabilities,
|
|
459
489
|
releaseChecks: adapter.releaseChecks,
|
|
460
490
|
metadata: adapter.metadata
|
|
461
491
|
}));
|
|
@@ -561,6 +591,9 @@ function validateTreeseedPackageManifests(root = workspaceRoot()) {
|
|
|
561
591
|
if (stringValue(deploymentSource.staging) !== "git") {
|
|
562
592
|
errors.push("docker package must declare deploymentSource.staging: git");
|
|
563
593
|
}
|
|
594
|
+
if (adapter.capabilities.localOnly && adapter.capabilities.deploy) {
|
|
595
|
+
errors.push("local-only package cannot declare hosted deployment capability");
|
|
596
|
+
}
|
|
564
597
|
if (stringValue(deploymentSource.prod) !== "image") {
|
|
565
598
|
errors.push("docker package must declare deploymentSource.prod: image");
|
|
566
599
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type TreeseedPackageArtifactManifest = {
|
|
2
|
+
schemaVersion: 1;
|
|
3
|
+
kind: 'treeseed.package-artifact';
|
|
4
|
+
packageName: string;
|
|
5
|
+
packageVersion: string;
|
|
6
|
+
sourceSha: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
file: string;
|
|
9
|
+
sha256: string;
|
|
10
|
+
size: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildTreeseedPackageArtifact(input: {
|
|
13
|
+
packageRoot: string;
|
|
14
|
+
outputDir: string;
|
|
15
|
+
}): {
|
|
16
|
+
artifactPath: string;
|
|
17
|
+
manifestPath: string;
|
|
18
|
+
manifest: TreeseedPackageArtifactManifest;
|
|
19
|
+
};
|
|
20
|
+
export declare function verifyTreeseedPackageArtifact(input: {
|
|
21
|
+
manifestPath: string;
|
|
22
|
+
artifactPath?: string;
|
|
23
|
+
}): {
|
|
24
|
+
ok: true;
|
|
25
|
+
artifactPath: string;
|
|
26
|
+
manifestPath: string;
|
|
27
|
+
manifest: TreeseedPackageArtifactManifest;
|
|
28
|
+
};
|
|
29
|
+
export declare function hydrateTreeseedPackageArtifacts(input: {
|
|
30
|
+
artifactsRoot: string;
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
}): {
|
|
33
|
+
ok: true;
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
artifactsRoot: string;
|
|
36
|
+
packages: TreeseedPackageArtifactManifest[];
|
|
37
|
+
};
|