@treeseed/sdk 0.10.28 → 0.11.0
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/README.md +207 -6
- package/dist/capacity-provider.d.ts +3 -1
- package/dist/capacity-provider.js +25 -5
- package/dist/control-plane.d.ts +1 -0
- package/dist/control-plane.js +38 -13
- package/dist/db/market-schema.d.ts +8860 -6172
- package/dist/db/market-schema.js +108 -0
- package/dist/db/node-sqlite.js +7 -2
- package/dist/hosting/apps.d.ts +12 -0
- package/dist/hosting/apps.js +107 -0
- package/dist/hosting/builtins.d.ts +25 -0
- package/dist/hosting/builtins.js +791 -0
- package/dist/hosting/contracts.d.ts +207 -0
- package/dist/hosting/contracts.js +0 -0
- package/dist/hosting/graph.d.ts +192 -0
- package/dist/hosting/graph.js +1106 -0
- package/dist/hosting/index.d.ts +4 -0
- package/dist/hosting/index.js +4 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +63 -6
- package/dist/managed-dependencies.js +1 -2
- package/dist/market-client.d.ts +63 -3
- package/dist/market-client.js +83 -11
- package/dist/operations/services/bootstrap-runner.d.ts +3 -1
- package/dist/operations/services/bootstrap-runner.js +22 -2
- package/dist/operations/services/config-runtime.d.ts +10 -5
- package/dist/operations/services/config-runtime.js +209 -66
- package/dist/operations/services/deploy.d.ts +70 -7
- package/dist/operations/services/deploy.js +579 -64
- package/dist/operations/services/deployment-readiness.d.ts +30 -0
- package/dist/operations/services/deployment-readiness.js +175 -0
- package/dist/operations/services/git-workflow.d.ts +2 -1
- package/dist/operations/services/git-workflow.js +9 -3
- package/dist/operations/services/github-actions-verification.d.ts +1 -0
- package/dist/operations/services/github-actions-verification.js +1 -0
- package/dist/operations/services/github-api.js +1 -1
- package/dist/operations/services/github-automation.d.ts +1 -1
- package/dist/operations/services/github-automation.js +4 -3
- package/dist/operations/services/github-credentials.d.ts +13 -0
- package/dist/operations/services/github-credentials.js +58 -0
- package/dist/operations/services/hosted-service-checks.d.ts +63 -0
- package/dist/operations/services/hosted-service-checks.js +327 -0
- package/dist/operations/services/hub-provider-launch.js +3 -3
- package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
- package/dist/operations/services/live-hosted-service-checks.js +350 -0
- package/dist/operations/services/managed-host-security.js +1 -1
- package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
- package/dist/operations/services/operations-runner-smoke.js +180 -0
- package/dist/operations/services/package-adapters.d.ts +95 -0
- package/dist/operations/services/package-adapters.js +288 -0
- package/dist/operations/services/package-reference-policy.d.ts +1 -0
- package/dist/operations/services/package-reference-policy.js +15 -2
- package/dist/operations/services/project-platform.d.ts +80 -22
- package/dist/operations/services/project-platform.js +49 -8
- package/dist/operations/services/project-web-monitor.js +26 -4
- package/dist/operations/services/railway-api.d.ts +88 -5
- package/dist/operations/services/railway-api.js +626 -35
- package/dist/operations/services/railway-deploy.d.ts +46 -40
- package/dist/operations/services/railway-deploy.js +261 -293
- package/dist/operations/services/release-candidate.d.ts +19 -0
- package/dist/operations/services/release-candidate.js +375 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
- package/dist/operations/services/repository-save-orchestrator.js +279 -66
- package/dist/operations/services/runtime-tools.d.ts +1 -0
- package/dist/operations/services/runtime-tools.js +10 -9
- package/dist/operations/services/verification-cache.d.ts +25 -0
- package/dist/operations/services/verification-cache.js +71 -0
- package/dist/operations/services/workspace-dependency-mode.js +9 -1
- package/dist/operations/services/workspace-save.js +1 -1
- package/dist/operations/services/workspace-tools.js +2 -1
- package/dist/platform/contracts.d.ts +32 -1
- package/dist/platform/deploy-config.js +73 -8
- package/dist/platform/env.yaml +163 -35
- package/dist/platform/environment.d.ts +1 -0
- package/dist/platform/environment.js +74 -5
- package/dist/platform/plugin.d.ts +9 -0
- package/dist/platform-operation-store.js +2 -2
- package/dist/platform-operations.js +1 -1
- package/dist/reconcile/bootstrap-systems.js +2 -2
- package/dist/reconcile/builtin-adapters.js +372 -189
- package/dist/reconcile/contracts.d.ts +9 -5
- package/dist/reconcile/desired-state.d.ts +1 -0
- package/dist/reconcile/desired-state.js +5 -5
- package/dist/reconcile/engine.d.ts +5 -2
- package/dist/reconcile/engine.js +53 -32
- package/dist/reconcile/index.d.ts +2 -0
- package/dist/reconcile/index.js +2 -0
- package/dist/reconcile/live-acceptance.d.ts +79 -0
- package/dist/reconcile/live-acceptance.js +1615 -0
- package/dist/reconcile/platform.d.ts +104 -0
- package/dist/reconcile/platform.js +100 -0
- package/dist/reconcile/state.js +4 -4
- package/dist/reconcile/units.js +2 -2
- package/dist/scripts/deployment-readiness.js +20 -0
- package/dist/scripts/generate-treedx-openapi-types.js +186 -0
- package/dist/scripts/operations-runner-smoke.js +16 -0
- package/dist/scripts/release-verify.js +4 -1
- package/dist/scripts/tenant-workflow-action.js +10 -1
- package/dist/sdk-types.d.ts +169 -4
- package/dist/sdk-types.js +20 -2
- package/dist/sdk.d.ts +35 -24
- package/dist/sdk.js +186 -17
- package/dist/template-launch-requirements.js +9 -0
- package/dist/treedx/adapters.d.ts +6 -0
- package/dist/treedx/adapters.js +36 -0
- package/dist/treedx/client.d.ts +222 -0
- package/dist/treedx/client.js +871 -0
- package/dist/treedx/errors.d.ts +13 -0
- package/dist/treedx/errors.js +17 -0
- package/dist/treedx/federated-client.d.ts +27 -0
- package/dist/treedx/federated-client.js +158 -0
- package/dist/treedx/generated/openapi-types.d.ts +3558 -0
- package/dist/treedx/generated/openapi-types.js +0 -0
- package/dist/treedx/graph-adapter.d.ts +33 -0
- package/dist/treedx/graph-adapter.js +156 -0
- package/dist/treedx/index.d.ts +14 -0
- package/dist/treedx/index.js +48 -0
- package/dist/treedx/market-integration.d.ts +27 -0
- package/dist/treedx/market-integration.js +131 -0
- package/dist/treedx/ports.d.ts +166 -0
- package/dist/treedx/ports.js +231 -0
- package/dist/treedx/query-adapter.d.ts +19 -0
- package/dist/treedx/query-adapter.js +62 -0
- package/dist/treedx/registry-client.d.ts +11 -0
- package/dist/treedx/registry-client.js +19 -0
- package/dist/treedx/repository-adapter.d.ts +45 -0
- package/dist/treedx/repository-adapter.js +308 -0
- package/dist/treedx/sdk-integration.d.ts +27 -0
- package/dist/treedx/sdk-integration.js +63 -0
- package/dist/treedx/types.d.ts +1084 -0
- package/dist/treedx/types.js +8 -0
- package/dist/treedx/workspace-adapter.d.ts +27 -0
- package/dist/treedx/workspace-adapter.js +65 -0
- package/dist/treedx-backends.d.ts +218 -0
- package/dist/treedx-backends.js +632 -0
- package/dist/treedx-client.d.ts +86 -0
- package/dist/treedx-client.js +175 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
- package/dist/workflow/operations.d.ts +119 -13
- package/dist/workflow/operations.js +309 -53
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +43 -26
- package/dist/workflow-support.d.ts +11 -3
- package/dist/workflow-support.js +67 -3
- package/dist/workflow.d.ts +5 -0
- package/drizzle/market/0004_treedx_market_integration.sql +99 -0
- package/package.json +34 -3
- package/templates/github/deploy-web.workflow.yml +39 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ReleaseCandidateStatus = 'passed' | 'failed';
|
|
2
|
+
export type ReleaseCandidateMode = 'hybrid' | 'strict' | 'skip';
|
|
2
3
|
export type ReleaseCandidateFailure = {
|
|
3
4
|
code: string;
|
|
4
5
|
scope: string;
|
|
@@ -15,6 +16,14 @@ export type ReleaseCandidateFingerprint = {
|
|
|
15
16
|
lockfiles: Record<string, string | null>;
|
|
16
17
|
selectedPackages: string[];
|
|
17
18
|
};
|
|
19
|
+
export type ReleaseCandidateTopologyFingerprint = {
|
|
20
|
+
key: string;
|
|
21
|
+
policyVersion: string;
|
|
22
|
+
packageManifests: Record<string, string | null>;
|
|
23
|
+
lockfiles: Record<string, string | null>;
|
|
24
|
+
treeseedManifests: Record<string, string | null>;
|
|
25
|
+
selectedPackages: string[];
|
|
26
|
+
};
|
|
18
27
|
export type ReleaseCandidateCheck = {
|
|
19
28
|
name: string;
|
|
20
29
|
status: 'passed' | 'skipped' | 'failed';
|
|
@@ -23,6 +32,9 @@ export type ReleaseCandidateCheck = {
|
|
|
23
32
|
export type ReleaseCandidateReport = {
|
|
24
33
|
status: ReleaseCandidateStatus;
|
|
25
34
|
fingerprint: ReleaseCandidateFingerprint;
|
|
35
|
+
mode: ReleaseCandidateMode;
|
|
36
|
+
reason: string;
|
|
37
|
+
topology: ReleaseCandidateTopologyFingerprint;
|
|
26
38
|
reused: boolean;
|
|
27
39
|
checkedAt: string;
|
|
28
40
|
failures: ReleaseCandidateFailure[];
|
|
@@ -33,9 +45,16 @@ export type ReleaseCandidateInput = {
|
|
|
33
45
|
plannedVersions: Record<string, unknown>;
|
|
34
46
|
selectedPackageNames?: string[];
|
|
35
47
|
allowReuse?: boolean;
|
|
48
|
+
mode?: ReleaseCandidateMode;
|
|
36
49
|
};
|
|
37
50
|
export declare function buildReleaseCandidateFingerprint(input: ReleaseCandidateInput): ReleaseCandidateFingerprint;
|
|
51
|
+
export declare function buildReleaseCandidateTopologyFingerprint(input: ReleaseCandidateInput): ReleaseCandidateTopologyFingerprint;
|
|
38
52
|
export declare function readCachedReleaseCandidateReport(root: string, key: string): ReleaseCandidateReport | null;
|
|
39
53
|
export declare function writeReleaseCandidateReport(root: string, report: ReleaseCandidateReport): ReleaseCandidateReport;
|
|
40
54
|
export declare function collectReleaseCandidateOutputFailures(line: string): string[];
|
|
55
|
+
export declare function isRootWebReleaseCandidateEntry(entry: {
|
|
56
|
+
id: string;
|
|
57
|
+
group?: string | null;
|
|
58
|
+
serviceTargets?: unknown;
|
|
59
|
+
}): boolean;
|
|
41
60
|
export declare function runReleaseCandidateGate(input: ReleaseCandidateInput): Promise<ReleaseCandidateReport>;
|
|
@@ -6,15 +6,20 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
6
6
|
import { isTreeseedEnvironmentEntryRelevant, isTreeseedEnvironmentEntryRequired } from "../../platform/environment.js";
|
|
7
7
|
import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
|
|
8
8
|
import { createGitHubApiClient, listGitHubEnvironmentSecretNames, listGitHubEnvironmentVariableNames } from "./github-api.js";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveGitHubCredentialForRepository } from "./github-credentials.js";
|
|
10
|
+
import { collectInternalDevReferenceIssues, installableInternalDependencyVersions, normalizeGitRemoteForManifest } from "./package-reference-policy.js";
|
|
10
11
|
import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues, validateTreeseedCommandEnvironment } from "./config-runtime.js";
|
|
11
12
|
import { loadDeployState } from "./deploy.js";
|
|
12
13
|
import { loadCliDeployConfig } from "./runtime-tools.js";
|
|
13
14
|
import { packagesWithScript, run, workspacePackages } from "./workspace-tools.js";
|
|
14
15
|
import { createBuildWarningSummary, formatAllowedBuildWarnings } from "./build-warning-policy.js";
|
|
16
|
+
import { discoverTreeseedPackageAdapters } from "./package-adapters.js";
|
|
15
17
|
const RELEASE_CANDIDATE_CACHE_DIR = ".treeseed/workflow/release-candidates";
|
|
16
|
-
const RELEASE_CANDIDATE_POLICY_VERSION = "
|
|
18
|
+
const RELEASE_CANDIDATE_POLICY_VERSION = "package-adapters-v2-hybrid";
|
|
19
|
+
const RELEASE_CANDIDATE_TOPOLOGY_POLICY_VERSION = "topology-v1";
|
|
17
20
|
const STABLE_SEMVER = /^\d+\.\d+\.\d+$/u;
|
|
21
|
+
const INTERNAL_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"];
|
|
22
|
+
const TOPOLOGY_SCRIPT_PREFIXES = ["build", "check", "prepare", "prepack", "postpack", "release:", "verify", "sandbox:"];
|
|
18
23
|
const REHEARSAL_IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
19
24
|
".git",
|
|
20
25
|
".treeseed",
|
|
@@ -24,6 +29,22 @@ const REHEARSAL_IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
24
29
|
"dist",
|
|
25
30
|
"node_modules"
|
|
26
31
|
]);
|
|
32
|
+
const ROOT_WEB_EXCLUDED_DEPLOY_CONFIG_IDS = /* @__PURE__ */ new Set([
|
|
33
|
+
"DOCKERHUB_TOKEN",
|
|
34
|
+
"SECRET_KEY_BASE",
|
|
35
|
+
"TREEDX_JWT_HS256_SECRET",
|
|
36
|
+
"TREESEED_CREDENTIAL_SESSION_SECRET",
|
|
37
|
+
"TREESEED_PLATFORM_RUNNER_SECRET"
|
|
38
|
+
]);
|
|
39
|
+
const API_APP_SERVICE_TARGETS = /* @__PURE__ */ new Set([
|
|
40
|
+
"api",
|
|
41
|
+
"operationsRunner",
|
|
42
|
+
"marketOperationsRunner",
|
|
43
|
+
"publicTreeDxFederation",
|
|
44
|
+
"publicTreeDxNode",
|
|
45
|
+
"treedx",
|
|
46
|
+
"treeDx"
|
|
47
|
+
]);
|
|
27
48
|
function nowIso() {
|
|
28
49
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
29
50
|
}
|
|
@@ -51,6 +72,9 @@ function safePackageJson(filePath) {
|
|
|
51
72
|
return null;
|
|
52
73
|
}
|
|
53
74
|
}
|
|
75
|
+
function dockerManifestCheckMode() {
|
|
76
|
+
return process.env.TREESEED_RELEASE_CANDIDATE_DOCKER_MANIFEST_MODE === "check" ? "check" : "skip";
|
|
77
|
+
}
|
|
54
78
|
function writeJsonFile(filePath, value) {
|
|
55
79
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
56
80
|
`, "utf8");
|
|
@@ -62,6 +86,9 @@ function packageScripts(filePath) {
|
|
|
62
86
|
function releaseCandidateCachePath(root, key) {
|
|
63
87
|
return resolve(root, RELEASE_CANDIDATE_CACHE_DIR, `${key}.json`);
|
|
64
88
|
}
|
|
89
|
+
function releaseCandidateTopologyCachePath(root, key) {
|
|
90
|
+
return resolve(root, RELEASE_CANDIDATE_CACHE_DIR, `topology-${key}.json`);
|
|
91
|
+
}
|
|
65
92
|
function ensureReleaseCandidateCacheDir(root) {
|
|
66
93
|
const dir = resolve(root, RELEASE_CANDIDATE_CACHE_DIR);
|
|
67
94
|
mkdirSync(dir, { recursive: true });
|
|
@@ -75,16 +102,19 @@ function ensureReleaseCandidateCacheDir(root) {
|
|
|
75
102
|
function buildReleaseCandidateFingerprint(input) {
|
|
76
103
|
const selectedPackages = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
|
|
77
104
|
const selectedPackageSet = new Set(selectedPackages);
|
|
78
|
-
const packages =
|
|
105
|
+
const packages = discoverTreeseedPackageAdapters(input.root).filter((pkg) => selectedPackageSet.size === 0 || selectedPackageSet.has(pkg.id) || selectedPackageSet.has(pkg.name));
|
|
79
106
|
const packageShas = sortedRecord(Object.fromEntries(
|
|
80
|
-
packages.map((pkg) => [pkg.
|
|
107
|
+
packages.map((pkg) => [pkg.id, safeGitHead(pkg.dir)])
|
|
81
108
|
));
|
|
82
109
|
const plannedVersions = sortedRecord(Object.fromEntries(
|
|
83
110
|
Object.entries(input.plannedVersions).filter(([name]) => name === "@treeseed/market" || selectedPackageSet.has(name)).map(([name, version]) => [name, String(version)])
|
|
84
111
|
));
|
|
85
112
|
const lockfiles = sortedRecord({
|
|
86
113
|
"@treeseed/market": fileSha256(resolve(input.root, "package-lock.json")),
|
|
87
|
-
...Object.fromEntries(packages.map((pkg) => [
|
|
114
|
+
...Object.fromEntries(packages.map((pkg) => [
|
|
115
|
+
pkg.id,
|
|
116
|
+
pkg.kind === "node-typescript" ? fileSha256(resolve(pkg.dir, "package-lock.json")) : fileSha256(resolve(pkg.dir, "Cargo.lock")) ?? fileSha256(resolve(pkg.dir, "mix.lock"))
|
|
117
|
+
]))
|
|
88
118
|
});
|
|
89
119
|
const base = {
|
|
90
120
|
policyVersion: RELEASE_CANDIDATE_POLICY_VERSION,
|
|
@@ -99,6 +129,123 @@ function buildReleaseCandidateFingerprint(input) {
|
|
|
99
129
|
key: sha256(JSON.stringify(base))
|
|
100
130
|
};
|
|
101
131
|
}
|
|
132
|
+
function isInternalTreeseedPackageName(name, internalPackageNames) {
|
|
133
|
+
return internalPackageNames.has(name) || name.startsWith("@treeseed/");
|
|
134
|
+
}
|
|
135
|
+
function normalizeDependencySpecForTopology(name, spec, internalPackageNames) {
|
|
136
|
+
if (!isInternalTreeseedPackageName(name, internalPackageNames)) return spec;
|
|
137
|
+
const value = String(spec ?? "").trim();
|
|
138
|
+
if (!value) return value;
|
|
139
|
+
if (/^(?:git\+|github:|gitlab:|bitbucket:|ssh:\/\/|https:\/\/|file:)/u.test(value) || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value) || /^workspace:/u.test(value)) {
|
|
140
|
+
return "<internal-treeseed-reference>";
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function normalizePackageJsonForTopology(packageJson, internalPackageNames) {
|
|
145
|
+
const normalized = {};
|
|
146
|
+
for (const [key, value] of Object.entries(packageJson)) {
|
|
147
|
+
if (key === "version") continue;
|
|
148
|
+
if (INTERNAL_DEPENDENCY_FIELDS.includes(key) && value && typeof value === "object" && !Array.isArray(value)) {
|
|
149
|
+
normalized[key] = sortedRecord(Object.fromEntries(Object.entries(value).map(([dependencyName, spec]) => [dependencyName, normalizeDependencySpecForTopology(dependencyName, spec, internalPackageNames)])));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (key === "scripts" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
153
|
+
normalized[key] = sortedRecord(Object.fromEntries(Object.entries(value).filter(([scriptName]) => TOPOLOGY_SCRIPT_PREFIXES.some((prefix) => scriptName === prefix || scriptName.startsWith(prefix))).map(([scriptName, command]) => [scriptName, command])));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (["name", "type", "private", "workspaces", "main", "module", "exports", "files", "bin", "publishConfig", "repository", "engines", "packageManager"].includes(key)) {
|
|
157
|
+
normalized[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return sortedRecord(normalized);
|
|
161
|
+
}
|
|
162
|
+
function normalizePackageLockForTopology(lockfile, internalPackageNames) {
|
|
163
|
+
const packages = lockfile.packages && typeof lockfile.packages === "object" && !Array.isArray(lockfile.packages) ? lockfile.packages : {};
|
|
164
|
+
const normalizedPackages = {};
|
|
165
|
+
for (const [path, entry] of Object.entries(packages)) {
|
|
166
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
167
|
+
const record = entry;
|
|
168
|
+
const packageName = typeof record.name === "string" ? record.name : path.startsWith("node_modules/") ? path.replace(/^node_modules\//u, "") : null;
|
|
169
|
+
const normalized = {};
|
|
170
|
+
for (const key of ["name", "link", "dev", "optional", "peer"]) {
|
|
171
|
+
if (key in record) normalized[key] = record[key];
|
|
172
|
+
}
|
|
173
|
+
for (const field of INTERNAL_DEPENDENCY_FIELDS) {
|
|
174
|
+
const values = record[field];
|
|
175
|
+
if (values && typeof values === "object" && !Array.isArray(values)) {
|
|
176
|
+
normalized[field] = sortedRecord(Object.fromEntries(Object.entries(values).map(([dependencyName, spec]) => [dependencyName, normalizeDependencySpecForTopology(dependencyName, spec, internalPackageNames)])));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (packageName && isInternalTreeseedPackageName(packageName, internalPackageNames)) {
|
|
180
|
+
normalized.version = "<internal-treeseed-reference>";
|
|
181
|
+
normalized.resolved = "<internal-treeseed-reference>";
|
|
182
|
+
normalized.integrity = "<internal-treeseed-reference>";
|
|
183
|
+
} else {
|
|
184
|
+
for (const key of ["version", "resolved", "integrity", "license"]) {
|
|
185
|
+
if (key in record) normalized[key] = record[key];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
normalizedPackages[path] = sortedRecord(normalized);
|
|
189
|
+
}
|
|
190
|
+
return sortedRecord({
|
|
191
|
+
name: lockfile.name,
|
|
192
|
+
lockfileVersion: lockfile.lockfileVersion,
|
|
193
|
+
requires: lockfile.requires,
|
|
194
|
+
packages: sortedRecord(normalizedPackages)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function topologyJsonHash(value) {
|
|
198
|
+
return sha256(JSON.stringify(value));
|
|
199
|
+
}
|
|
200
|
+
function topologyPackageHash(packageJsonPath, internalPackageNames) {
|
|
201
|
+
const packageJson = safePackageJson(packageJsonPath);
|
|
202
|
+
if (!packageJson) return null;
|
|
203
|
+
return topologyJsonHash(normalizePackageJsonForTopology(packageJson, internalPackageNames));
|
|
204
|
+
}
|
|
205
|
+
function topologyLockfileHash(lockfilePath, internalPackageNames) {
|
|
206
|
+
const lockfile = safePackageJson(lockfilePath);
|
|
207
|
+
if (!lockfile) return null;
|
|
208
|
+
return topologyJsonHash(normalizePackageLockForTopology(lockfile, internalPackageNames));
|
|
209
|
+
}
|
|
210
|
+
function buildReleaseCandidateTopologyFingerprint(input) {
|
|
211
|
+
const selectedPackages = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
|
|
212
|
+
const selectedPackageSet = new Set(selectedPackages);
|
|
213
|
+
const packages = discoverTreeseedPackageAdapters(input.root).filter((pkg) => selectedPackageSet.size === 0 || selectedPackageSet.has(pkg.id) || selectedPackageSet.has(pkg.name));
|
|
214
|
+
const internalPackageNames = /* @__PURE__ */ new Set([
|
|
215
|
+
"@treeseed/market",
|
|
216
|
+
...discoverTreeseedPackageAdapters(input.root).map((pkg) => pkg.name)
|
|
217
|
+
]);
|
|
218
|
+
const packageManifests = sortedRecord({
|
|
219
|
+
"@treeseed/market": topologyPackageHash(resolve(input.root, "package.json"), internalPackageNames),
|
|
220
|
+
...Object.fromEntries(packages.map((pkg) => [pkg.id, topologyPackageHash(resolve(pkg.dir, "package.json"), internalPackageNames)]))
|
|
221
|
+
});
|
|
222
|
+
const lockfiles = sortedRecord({
|
|
223
|
+
"@treeseed/market": topologyLockfileHash(resolve(input.root, "package-lock.json"), internalPackageNames),
|
|
224
|
+
...Object.fromEntries(packages.map((pkg) => [
|
|
225
|
+
pkg.id,
|
|
226
|
+
pkg.kind === "node-typescript" ? topologyLockfileHash(resolve(pkg.dir, "package-lock.json"), internalPackageNames) : fileSha256(resolve(pkg.dir, "Cargo.lock")) ?? fileSha256(resolve(pkg.dir, "mix.lock"))
|
|
227
|
+
]))
|
|
228
|
+
});
|
|
229
|
+
const manifestEntries = {
|
|
230
|
+
"treeseed.site.yaml": fileSha256(resolve(input.root, "treeseed.site.yaml")),
|
|
231
|
+
"treeseed.package.yaml": fileSha256(resolve(input.root, "treeseed.package.yaml"))
|
|
232
|
+
};
|
|
233
|
+
for (const pkg of packages) {
|
|
234
|
+
manifestEntries[`${pkg.id}:treeseed.package.yaml`] = fileSha256(resolve(pkg.dir, "treeseed.package.yaml"));
|
|
235
|
+
manifestEntries[`${pkg.id}:treeseed.site.yaml`] = fileSha256(resolve(pkg.dir, "treeseed.site.yaml"));
|
|
236
|
+
}
|
|
237
|
+
const base = {
|
|
238
|
+
policyVersion: RELEASE_CANDIDATE_TOPOLOGY_POLICY_VERSION,
|
|
239
|
+
packageManifests,
|
|
240
|
+
lockfiles,
|
|
241
|
+
treeseedManifests: sortedRecord(manifestEntries),
|
|
242
|
+
selectedPackages
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
...base,
|
|
246
|
+
key: sha256(JSON.stringify(base))
|
|
247
|
+
};
|
|
248
|
+
}
|
|
102
249
|
function readCachedReleaseCandidateReport(root, key) {
|
|
103
250
|
const cachePath = releaseCandidateCachePath(root, key);
|
|
104
251
|
if (!existsSync(cachePath)) return null;
|
|
@@ -112,8 +259,27 @@ function writeReleaseCandidateReport(root, report) {
|
|
|
112
259
|
ensureReleaseCandidateCacheDir(root);
|
|
113
260
|
writeFileSync(releaseCandidateCachePath(root, report.fingerprint.key), `${JSON.stringify(report, null, 2)}
|
|
114
261
|
`, "utf8");
|
|
262
|
+
if (report.status === "passed" && report.mode === "strict") {
|
|
263
|
+
writeFileSync(releaseCandidateTopologyCachePath(root, report.topology.key), `${JSON.stringify({
|
|
264
|
+
key: report.topology.key,
|
|
265
|
+
checkedAt: report.checkedAt,
|
|
266
|
+
fingerprintKey: report.fingerprint.key,
|
|
267
|
+
mode: report.mode,
|
|
268
|
+
reason: report.reason
|
|
269
|
+
}, null, 2)}
|
|
270
|
+
`, "utf8");
|
|
271
|
+
}
|
|
115
272
|
return report;
|
|
116
273
|
}
|
|
274
|
+
function readStrictTopologyProof(root, key) {
|
|
275
|
+
const cachePath = releaseCandidateTopologyCachePath(root, key);
|
|
276
|
+
if (!existsSync(cachePath)) return null;
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(readFileSync(cachePath, "utf8"));
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
117
283
|
function addFailure(failures, failure) {
|
|
118
284
|
failures.push({
|
|
119
285
|
...failure,
|
|
@@ -121,53 +287,108 @@ function addFailure(failures, failure) {
|
|
|
121
287
|
provider: failure.provider ?? null
|
|
122
288
|
});
|
|
123
289
|
}
|
|
124
|
-
function packageReadinessChecks(root, selectedPackageNames, failures) {
|
|
290
|
+
function packageReadinessChecks(root, selectedPackageNames, failures, options = {}) {
|
|
125
291
|
if (selectedPackageNames.length === 0) {
|
|
126
292
|
return { name: "package-release-readiness", status: "skipped", detail: "No packages are selected for this release." };
|
|
127
293
|
}
|
|
128
294
|
const selected = new Set(selectedPackageNames);
|
|
129
|
-
const packages =
|
|
295
|
+
const packages = discoverTreeseedPackageAdapters(root).filter((pkg) => selected.has(pkg.id) || selected.has(pkg.name));
|
|
130
296
|
for (const pkg of packages) {
|
|
297
|
+
checkPackageAdapterReadiness(pkg, failures, options);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
name: "package-release-readiness",
|
|
301
|
+
status: failures.some((failure) => failure.code.startsWith("missing_") || failure.code === "npm_pack_dry_run_failed" || failure.code === "docker_manifest_check_failed") ? "failed" : "passed",
|
|
302
|
+
detail: `Checked ${packages.length} selected package adapter${packages.length === 1 ? "" : "s"}${options.skipNpmPack ? " without npm pack rehearsal" : ""}: ${packages.map((pkg) => `${pkg.id} (${pkg.kind})`).join(", ") || "none"}.`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function checkPackageAdapterReadiness(pkg, failures, options = {}) {
|
|
306
|
+
if (pkg.kind === "node-typescript") {
|
|
131
307
|
const packageJson = safePackageJson(resolve(pkg.dir, "package.json"));
|
|
132
308
|
const scripts = packageJson?.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : {};
|
|
133
309
|
if (!existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))) {
|
|
134
310
|
addFailure(failures, {
|
|
135
311
|
code: "missing_publish_workflow",
|
|
136
|
-
scope: pkg.
|
|
312
|
+
scope: pkg.id,
|
|
137
313
|
provider: "github",
|
|
138
|
-
message: `${pkg.
|
|
314
|
+
message: `${pkg.id} is missing .github/workflows/publish.yml.`
|
|
139
315
|
});
|
|
140
316
|
}
|
|
141
317
|
if (typeof scripts["release:publish"] !== "string") {
|
|
142
318
|
addFailure(failures, {
|
|
143
319
|
code: "missing_publish_script",
|
|
144
|
-
scope: pkg.
|
|
145
|
-
message: `${pkg.
|
|
320
|
+
scope: pkg.id,
|
|
321
|
+
message: `${pkg.id} is missing a release:publish script.`
|
|
146
322
|
});
|
|
147
323
|
}
|
|
148
|
-
if (typeof scripts["verify:local"] !== "string" && typeof scripts
|
|
324
|
+
if (typeof scripts["verify:local"] !== "string" && typeof scripts.verify !== "string" && typeof scripts["verify:action"] !== "string") {
|
|
149
325
|
addFailure(failures, {
|
|
150
326
|
code: "missing_verify_script",
|
|
151
|
-
scope: pkg.
|
|
152
|
-
message: `${pkg.
|
|
327
|
+
scope: pkg.id,
|
|
328
|
+
message: `${pkg.id} is missing a release-ready verify script.`
|
|
153
329
|
});
|
|
154
330
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
331
|
+
if (!options.skipNpmPack) {
|
|
332
|
+
try {
|
|
333
|
+
run("npm", ["pack", "--dry-run"], { cwd: pkg.dir, capture: true, timeoutMs: 12e4 });
|
|
334
|
+
} catch (error) {
|
|
335
|
+
addFailure(failures, {
|
|
336
|
+
code: "npm_pack_dry_run_failed",
|
|
337
|
+
scope: pkg.id,
|
|
338
|
+
message: `${pkg.id} failed npm pack --dry-run.`,
|
|
339
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!pkg.version) {
|
|
346
|
+
addFailure(failures, {
|
|
347
|
+
code: "missing_package_version",
|
|
348
|
+
scope: pkg.id,
|
|
349
|
+
message: `${pkg.id} is missing a readable BEAM package version.`,
|
|
350
|
+
details: { versionSource: pkg.versionSource }
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (pkg.id === "treedx" && !existsSync(resolve(pkg.dir, ".github", "workflows", "dev-image.yml"))) {
|
|
354
|
+
addFailure(failures, {
|
|
355
|
+
code: "missing_development_image_workflow",
|
|
356
|
+
scope: pkg.id,
|
|
357
|
+
provider: "github",
|
|
358
|
+
message: `${pkg.id} is missing .github/workflows/dev-image.yml for staging-safe development image publication.`
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (!pkg.verifyCommands.local) {
|
|
362
|
+
addFailure(failures, {
|
|
363
|
+
code: "missing_verify_script",
|
|
364
|
+
scope: pkg.id,
|
|
365
|
+
message: `${pkg.id} is missing a BEAM package local verification command.`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (!pkg.verifyCommands.release) {
|
|
369
|
+
addFailure(failures, {
|
|
370
|
+
code: "missing_release_gate",
|
|
371
|
+
scope: pkg.id,
|
|
372
|
+
message: `${pkg.id} is missing a BEAM package release gate command.`
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (dockerManifestCheckMode() !== "check") return;
|
|
376
|
+
for (const artifact of pkg.artifacts.filter((entry) => entry.provider === "docker")) {
|
|
377
|
+
for (const tag of artifact.tags ?? []) {
|
|
378
|
+
if (tag.includes("<")) continue;
|
|
379
|
+
try {
|
|
380
|
+
run("docker", ["manifest", "inspect", `${artifact.name}:${tag}`], { cwd: pkg.dir, capture: true, timeoutMs: 12e4 });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
addFailure(failures, {
|
|
383
|
+
code: "docker_manifest_check_failed",
|
|
384
|
+
scope: pkg.id,
|
|
385
|
+
provider: "docker",
|
|
386
|
+
message: `${pkg.id} Docker artifact is not published: ${artifact.name}:${tag}.`,
|
|
387
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
388
|
+
});
|
|
389
|
+
}
|
|
164
390
|
}
|
|
165
391
|
}
|
|
166
|
-
return {
|
|
167
|
-
name: "package-release-readiness",
|
|
168
|
-
status: failures.some((failure) => failure.code.startsWith("missing_") || failure.code === "npm_pack_dry_run_failed") ? "failed" : "passed",
|
|
169
|
-
detail: `Checked ${packages.length} selected package${packages.length === 1 ? "" : "s"}.`
|
|
170
|
-
};
|
|
171
392
|
}
|
|
172
393
|
function copyWorkspaceForProductionRehearsal(root) {
|
|
173
394
|
const tempParent = mkdtempSync(join(tmpdir(), "treeseed-release-candidate-"));
|
|
@@ -187,7 +408,8 @@ function applyPlannedStableMetadata(root, plannedVersions) {
|
|
|
187
408
|
const stableVersions = new Map(
|
|
188
409
|
Object.entries(plannedVersions).filter(([, version]) => STABLE_SEMVER.test(version))
|
|
189
410
|
);
|
|
190
|
-
const
|
|
411
|
+
const dependencyVersions = installableInternalDependencyVersions(root, stableVersions);
|
|
412
|
+
const stableGitReferences = stablePackageGitReferences(root, dependencyVersions);
|
|
191
413
|
const targets = [
|
|
192
414
|
{ name: "@treeseed/market", dir: root },
|
|
193
415
|
...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))
|
|
@@ -205,7 +427,7 @@ function applyPlannedStableMetadata(root, plannedVersions) {
|
|
|
205
427
|
for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
|
|
206
428
|
const values = packageJson[field];
|
|
207
429
|
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
208
|
-
for (const [dependencyName, version] of
|
|
430
|
+
for (const [dependencyName, version] of dependencyVersions.entries()) {
|
|
209
431
|
if (!(dependencyName in values)) continue;
|
|
210
432
|
const dependencySpec = stableGitReferences.get(dependencyName) ?? version;
|
|
211
433
|
if (String(values[dependencyName]) === dependencySpec) continue;
|
|
@@ -285,6 +507,15 @@ function runNpmRehearsalCommand(args, options) {
|
|
|
285
507
|
].join("\n"));
|
|
286
508
|
}
|
|
287
509
|
}
|
|
510
|
+
function npmRehearsalEnv(extra = {}) {
|
|
511
|
+
return {
|
|
512
|
+
...process.env,
|
|
513
|
+
npm_config_jobs: process.env.npm_config_jobs ?? "2",
|
|
514
|
+
npm_config_audit: process.env.npm_config_audit ?? "false",
|
|
515
|
+
npm_config_fund: process.env.npm_config_fund ?? "false",
|
|
516
|
+
...extra
|
|
517
|
+
};
|
|
518
|
+
}
|
|
288
519
|
function collectReleaseCandidateOutputFailures(line) {
|
|
289
520
|
const value = String(line ?? "").trim();
|
|
290
521
|
if (!value) return [];
|
|
@@ -315,17 +546,18 @@ function runProductionDependencyRehearsal(root, plannedVersions, selectedPackage
|
|
|
315
546
|
const copied = copyWorkspaceForProductionRehearsal(root);
|
|
316
547
|
tempParent = copied.tempParent;
|
|
317
548
|
applyPlannedStableMetadata(copied.tempRoot, plannedVersions);
|
|
318
|
-
|
|
319
|
-
runNpmRehearsalCommand(["
|
|
549
|
+
const npmEnv = npmRehearsalEnv();
|
|
550
|
+
runNpmRehearsalCommand(["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline"], { cwd: copied.tempRoot, timeoutMs: 3e5, env: npmEnv });
|
|
551
|
+
runNpmRehearsalCommand(["ci", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline"], { cwd: copied.tempRoot, timeoutMs: 6e5, env: npmEnv });
|
|
320
552
|
buildRehearsalWorkspacePackageArtifacts(copied.tempRoot);
|
|
321
553
|
const scriptName = rehearsalVerifyScript(copied.tempRoot);
|
|
322
554
|
if (scriptName) {
|
|
323
555
|
const packageJson = safePackageJson(resolve(copied.tempRoot, "package.json"));
|
|
324
|
-
const parallelMarketVerify = packageJson?.name === "@treeseed/market" && (scriptName === "verify:direct" || scriptName === "verify:local" || scriptName === "verify");
|
|
556
|
+
const parallelMarketVerify = process.env.TREESEED_RELEASE_CANDIDATE_MARKET_VERIFY_PARALLEL === "1" && packageJson?.name === "@treeseed/market" && (scriptName === "verify:direct" || scriptName === "verify:local" || scriptName === "verify");
|
|
325
557
|
runNpmRehearsalCommand(["run", scriptName], {
|
|
326
558
|
cwd: copied.tempRoot,
|
|
327
559
|
timeoutMs: 9e5,
|
|
328
|
-
env: parallelMarketVerify ? {
|
|
560
|
+
env: parallelMarketVerify ? npmRehearsalEnv({ TREESEED_VERIFY_PARALLEL: "1" }) : npmEnv
|
|
329
561
|
});
|
|
330
562
|
}
|
|
331
563
|
const postInstallIssues = collectInternalDevReferenceIssues(copied.tempRoot, selectedPackageSet);
|
|
@@ -400,11 +632,95 @@ function dependencyRehearsalChecks(root, plannedVersions, selectedPackageNames,
|
|
|
400
632
|
detail: `${devReferenceIssues.length > 0 ? `Rehearsed stable replacements for ${devReferenceIssues.length} internal dev reference${devReferenceIssues.length === 1 ? "" : "s"}.` : "Checked planned stable versions and internal dependency references."} ${rehearsalDetail}`
|
|
401
633
|
};
|
|
402
634
|
}
|
|
635
|
+
function validateInternalGitReferenceTags(root, failures) {
|
|
636
|
+
const issues = collectInternalDevReferenceIssues(root);
|
|
637
|
+
let checked = 0;
|
|
638
|
+
const seen = /* @__PURE__ */ new Set();
|
|
639
|
+
for (const issue of issues) {
|
|
640
|
+
const spec = issue.spec;
|
|
641
|
+
const hashIndex = spec.lastIndexOf("#");
|
|
642
|
+
if (hashIndex === -1 || !/^(?:git\+|github:|gitlab:|bitbucket:|ssh:\/\/|https:\/\/|file:)/u.test(spec)) continue;
|
|
643
|
+
const rawRemote = spec.slice(0, hashIndex).replace(/^git\+/u, "");
|
|
644
|
+
const githubMatch = rawRemote.match(/^github:([^/]+\/[^/]+?)(?:\.git)?$/u);
|
|
645
|
+
const remote = githubMatch ? `https://github.com/${githubMatch[1]}.git` : rawRemote;
|
|
646
|
+
const tagName = decodeURIComponent(spec.slice(hashIndex + 1));
|
|
647
|
+
if (!remote || !tagName) continue;
|
|
648
|
+
const key = `${remote}#${tagName}`;
|
|
649
|
+
if (seen.has(key)) continue;
|
|
650
|
+
seen.add(key);
|
|
651
|
+
checked += 1;
|
|
652
|
+
try {
|
|
653
|
+
run("git", ["ls-remote", "--exit-code", "--tags", remote, `refs/tags/${tagName}`], { cwd: root, capture: true, timeoutMs: 12e4 });
|
|
654
|
+
} catch (error) {
|
|
655
|
+
addFailure(failures, {
|
|
656
|
+
code: "internal_git_tag_missing",
|
|
657
|
+
scope: issue.dependencyName ?? issue.repoName,
|
|
658
|
+
provider: "git",
|
|
659
|
+
message: `Internal git dependency tag is not reachable: ${issue.dependencyName ?? issue.repoName}#${tagName}.`,
|
|
660
|
+
details: { spec, remote, tagName, filePath: issue.filePath, error: error instanceof Error ? error.message : String(error) }
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return checked;
|
|
665
|
+
}
|
|
666
|
+
function lightweightDependencyChecks(root, failures) {
|
|
667
|
+
const before = failures.length;
|
|
668
|
+
try {
|
|
669
|
+
runNpmRehearsalCommand(["install", "--package-lock-only", "--ignore-scripts", "--dry-run", "--workspaces=false", "--no-audit", "--no-fund", "--prefer-offline"], {
|
|
670
|
+
cwd: root,
|
|
671
|
+
timeoutMs: 3e5,
|
|
672
|
+
env: npmRehearsalEnv()
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
addFailure(failures, {
|
|
676
|
+
code: "lockfile_dry_run_failed",
|
|
677
|
+
scope: "@treeseed/market",
|
|
678
|
+
message: "Root lockfile dry-run validation failed.",
|
|
679
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const checkedTags = validateInternalGitReferenceTags(root, failures);
|
|
683
|
+
return {
|
|
684
|
+
name: "hybrid-dependency-readiness",
|
|
685
|
+
status: failures.length > before ? "failed" : "passed",
|
|
686
|
+
detail: `Validated root lockfile with npm install --package-lock-only --ignore-scripts --dry-run --workspaces=false and checked ${checkedTags} internal git tag${checkedTags === 1 ? "" : "s"} without temp install rehearsal.`
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function skippedReleaseCandidateReport(fingerprint, topology) {
|
|
690
|
+
return {
|
|
691
|
+
status: "passed",
|
|
692
|
+
fingerprint,
|
|
693
|
+
mode: "skip",
|
|
694
|
+
reason: "Release-candidate checks skipped by explicit request.",
|
|
695
|
+
topology,
|
|
696
|
+
reused: false,
|
|
697
|
+
checkedAt: nowIso(),
|
|
698
|
+
failures: [],
|
|
699
|
+
checks: [{
|
|
700
|
+
name: "release-candidate",
|
|
701
|
+
status: "skipped",
|
|
702
|
+
detail: "Skipped by --release-candidate skip or TREESEED_RELEASE_CANDIDATE_MODE=skip."
|
|
703
|
+
}]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function entryServiceTargets(entry) {
|
|
707
|
+
return Array.isArray(entry.serviceTargets) ? entry.serviceTargets.filter((target) => typeof target === "string") : [];
|
|
708
|
+
}
|
|
709
|
+
function isRootWebReleaseCandidateEntry(entry) {
|
|
710
|
+
if (ROOT_WEB_EXCLUDED_DEPLOY_CONFIG_IDS.has(entry.id)) return false;
|
|
711
|
+
const serviceTargets = entryServiceTargets(entry);
|
|
712
|
+
if (serviceTargets.length > 0 && serviceTargets.every((target) => API_APP_SERVICE_TARGETS.has(target))) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
if (entry.group === "docker") return false;
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
403
718
|
function localConfigCheck(root, scope, failures) {
|
|
404
719
|
try {
|
|
405
720
|
const report = validateTreeseedCommandEnvironment({ tenantRoot: root, scope, purpose: "deploy" });
|
|
406
721
|
const problems = [...report.validation.missing, ...report.validation.invalid];
|
|
407
722
|
for (const problem of problems) {
|
|
723
|
+
if (!isRootWebReleaseCandidateEntry(problem.entry)) continue;
|
|
408
724
|
addFailure(failures, {
|
|
409
725
|
code: "missing_local_config",
|
|
410
726
|
scope,
|
|
@@ -437,7 +753,11 @@ async function githubRemoteConfigCheck(root, scope, failures) {
|
|
|
437
753
|
try {
|
|
438
754
|
const environment = scope === "prod" ? "production" : scope;
|
|
439
755
|
const expected = expectedGitHubDeployEnvironment(root, scope);
|
|
440
|
-
const
|
|
756
|
+
const values = resolveTreeseedMachineEnvironmentValues(root, scope);
|
|
757
|
+
const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
|
|
758
|
+
const client = createGitHubApiClient({
|
|
759
|
+
env: credential.token ? { GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token } : process.env
|
|
760
|
+
});
|
|
441
761
|
const [secretNames, variableNames] = await Promise.all([
|
|
442
762
|
listGitHubEnvironmentSecretNames(repository, environment, { client }),
|
|
443
763
|
listGitHubEnvironmentVariableNames(repository, environment, { client })
|
|
@@ -476,6 +796,7 @@ function expectedGitHubDeployEnvironment(root, scope) {
|
|
|
476
796
|
const registry = collectTreeseedEnvironmentContext(root);
|
|
477
797
|
const values = resolveTreeseedMachineEnvironmentValues(root, scope);
|
|
478
798
|
const expectedEntries = registry.entries.filter((entry) => {
|
|
799
|
+
if (!isRootWebReleaseCandidateEntry(entry)) return false;
|
|
479
800
|
if (!isTreeseedEnvironmentEntryRelevant(entry, registry.context, scope, "deploy")) return false;
|
|
480
801
|
if (isTreeseedEnvironmentEntryRequired(entry, registry.context, scope, "deploy")) return true;
|
|
481
802
|
return typeof values[entry.id] === "string" && values[entry.id].trim().length > 0;
|
|
@@ -572,33 +893,47 @@ function migrationCompatibilityChecks(root, failures) {
|
|
|
572
893
|
return {
|
|
573
894
|
name: "migration-compatibility",
|
|
574
895
|
status: missing.length > 0 ? "failed" : "passed",
|
|
575
|
-
detail: "Checked required Drizzle migration artifacts for
|
|
896
|
+
detail: "Checked required Drizzle migration artifacts for Treeseed PostgreSQL and SDK D1."
|
|
576
897
|
};
|
|
577
898
|
}
|
|
578
899
|
async function runReleaseCandidateGate(input) {
|
|
579
900
|
const fingerprint = buildReleaseCandidateFingerprint(input);
|
|
901
|
+
const topology = buildReleaseCandidateTopologyFingerprint(input);
|
|
902
|
+
const requestedMode = input.mode ?? "strict";
|
|
580
903
|
if (input.allowReuse !== false) {
|
|
581
904
|
const cached = readCachedReleaseCandidateReport(input.root, fingerprint.key);
|
|
582
905
|
if (cached?.status === "passed") {
|
|
583
906
|
return {
|
|
584
907
|
...cached,
|
|
908
|
+
mode: cached.mode ?? "strict",
|
|
909
|
+
reason: cached.reason ?? "Reused cached release-candidate report.",
|
|
910
|
+
topology: cached.topology ?? topology,
|
|
585
911
|
reused: true
|
|
586
912
|
};
|
|
587
913
|
}
|
|
588
914
|
}
|
|
915
|
+
if (requestedMode === "skip") {
|
|
916
|
+
return skippedReleaseCandidateReport(fingerprint, topology);
|
|
917
|
+
}
|
|
589
918
|
const selectedPackageNames = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
|
|
590
919
|
const plannedVersions = Object.fromEntries(
|
|
591
920
|
Object.entries(input.plannedVersions).map(([name, version]) => [name, String(version)])
|
|
592
921
|
);
|
|
922
|
+
const strictTopologyProof = readStrictTopologyProof(input.root, topology.key);
|
|
923
|
+
const effectiveMode = requestedMode;
|
|
924
|
+
const reason = requestedMode === "hybrid" ? strictTopologyProof ? `Hybrid release-candidate selected; strict rehearsal skipped because topology ${topology.key.slice(0, 12)} was already proven.` : `Hybrid release-candidate selected; lightweight checks used because strict topology proof is reserved for promotion lanes.` : "Strict release-candidate selected.";
|
|
593
925
|
const failures = [];
|
|
594
926
|
const checks = [];
|
|
595
|
-
checks.push(dependencyRehearsalChecks(input.root, plannedVersions, selectedPackageNames, failures));
|
|
596
|
-
checks.push(packageReadinessChecks(input.root, selectedPackageNames, failures));
|
|
927
|
+
checks.push(effectiveMode === "hybrid" ? lightweightDependencyChecks(input.root, failures) : dependencyRehearsalChecks(input.root, plannedVersions, selectedPackageNames, failures));
|
|
928
|
+
checks.push(packageReadinessChecks(input.root, selectedPackageNames, failures, { skipNpmPack: effectiveMode === "hybrid" }));
|
|
597
929
|
checks.push(await configParityChecks(input.root, failures));
|
|
598
930
|
checks.push(migrationCompatibilityChecks(input.root, failures));
|
|
599
931
|
const report = {
|
|
600
932
|
status: failures.length === 0 ? "passed" : "failed",
|
|
601
933
|
fingerprint,
|
|
934
|
+
mode: effectiveMode,
|
|
935
|
+
reason,
|
|
936
|
+
topology,
|
|
602
937
|
reused: false,
|
|
603
938
|
checkedAt: nowIso(),
|
|
604
939
|
failures,
|
|
@@ -609,7 +944,9 @@ async function runReleaseCandidateGate(input) {
|
|
|
609
944
|
}
|
|
610
945
|
export {
|
|
611
946
|
buildReleaseCandidateFingerprint,
|
|
947
|
+
buildReleaseCandidateTopologyFingerprint,
|
|
612
948
|
collectReleaseCandidateOutputFailures,
|
|
949
|
+
isRootWebReleaseCandidateEntry,
|
|
613
950
|
readCachedReleaseCandidateReport,
|
|
614
951
|
runReleaseCandidateGate,
|
|
615
952
|
writeReleaseCandidateReport
|