@treeseed/sdk 0.10.23 → 0.10.25
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/index.d.ts +12 -2
- package/dist/index.js +42 -1
- package/dist/market-client.d.ts +23 -0
- package/dist/market-client.js +30 -0
- package/dist/operations/providers/default.js +103 -10
- package/dist/operations/repository-operations.d.ts +6 -1
- package/dist/operations/repository-operations.js +44 -0
- package/dist/operations/services/bootstrap-runner.d.ts +5 -1
- package/dist/operations/services/bootstrap-runner.js +34 -5
- package/dist/operations/services/config-runtime.d.ts +25 -9
- package/dist/operations/services/config-runtime.js +60 -12
- package/dist/operations/services/deploy.js +6 -1
- package/dist/operations/services/hub-launch.js +1 -0
- package/dist/operations/services/hub-provider-launch.d.ts +11 -1
- package/dist/operations/services/hub-provider-launch.js +81 -8
- package/dist/operations/services/project-host-operations.d.ts +153 -0
- package/dist/operations/services/project-host-operations.js +365 -0
- package/dist/operations/services/project-platform.d.ts +207 -177
- package/dist/operations/services/project-platform.js +96 -29
- package/dist/operations/services/railway-deploy.d.ts +33 -1
- package/dist/operations/services/railway-deploy.js +153 -44
- package/dist/operations/services/release-candidate.js +8 -2
- package/dist/operations/services/template-host-bindings.d.ts +68 -0
- package/dist/operations/services/template-host-bindings.js +400 -0
- package/dist/operations/services/template-registry.d.ts +22 -2
- package/dist/operations/services/template-registry.js +93 -6
- package/dist/operations/services/template-secret-sync.d.ts +97 -0
- package/dist/operations/services/template-secret-sync.js +292 -0
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +8 -1
- package/dist/platform/deploy-runtime.js +1 -0
- package/dist/platform/environment.d.ts +3 -0
- package/dist/project-workflow.d.ts +7 -1
- package/dist/reconcile/engine.d.ts +2 -0
- package/dist/reconcile/engine.js +58 -3
- package/dist/scripts/scaffold-site.js +3 -2
- package/dist/scripts/test-scaffold.js +2 -1
- package/dist/sdk-types.d.ts +87 -0
- package/dist/sdk-types.js +29 -0
- package/dist/template-catalog.js +3 -1
- package/dist/template-launch-requirements.d.ts +118 -0
- package/dist/template-launch-requirements.js +759 -0
- package/dist/template-launch-ui.d.ts +85 -0
- package/dist/template-launch-ui.js +189 -0
- package/dist/timing.d.ts +20 -0
- package/dist/timing.js +73 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +477 -0
- package/package.json +13 -1
- package/templates/github/deploy-web.workflow.yml +4 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { syncTreeseedCloudflareEnvironment, syncTreeseedGitHubEnvironment, syncTreeseedRailwayEnvironment, type TreeseedConfigScope } from './config-runtime.ts';
|
|
2
|
+
import type { ProjectLaunchResolvedHostBinding, ProjectLaunchSecretDeploymentPlanItem } from '../../template-launch-requirements.ts';
|
|
3
|
+
import type { ProjectLaunchRequirementKind } from '../../sdk-types.ts';
|
|
4
|
+
export type ProjectLaunchSecretSyncProvider = 'github' | 'cloudflare' | 'railway';
|
|
5
|
+
export type ProjectLaunchSecretSyncStatus = 'synced' | 'skipped' | 'failed';
|
|
6
|
+
export type ProjectLaunchSecretSyncProviderStatus = 'running' | 'completed' | 'failed';
|
|
7
|
+
export type ProjectLaunchSecretSyncTargetKind = 'github-secret' | 'github-variable' | 'cloudflare-secret' | 'cloudflare-var' | 'railway-secret' | 'railway-var';
|
|
8
|
+
export interface ProjectLaunchSecretValueDiagnostic {
|
|
9
|
+
code: 'missing_value' | 'unsupported_target';
|
|
10
|
+
requirementKey: string;
|
|
11
|
+
requirementKind: ProjectLaunchRequirementKind;
|
|
12
|
+
env: string;
|
|
13
|
+
source: string;
|
|
14
|
+
targets: string[];
|
|
15
|
+
scopes: TreeseedConfigScope[];
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ProjectLaunchResolvedSecretValueItem {
|
|
19
|
+
requirementKey: string;
|
|
20
|
+
requirementKind: ProjectLaunchRequirementKind;
|
|
21
|
+
env: string;
|
|
22
|
+
source: string;
|
|
23
|
+
targets: ProjectLaunchSecretSyncTargetKind[];
|
|
24
|
+
scopes: TreeseedConfigScope[];
|
|
25
|
+
sensitivity: string;
|
|
26
|
+
sourceHostId?: string | null;
|
|
27
|
+
resolved: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ProjectLaunchSecretValueOverlayResult {
|
|
30
|
+
valuesOverlay: Record<string, string>;
|
|
31
|
+
items: ProjectLaunchResolvedSecretValueItem[];
|
|
32
|
+
diagnostics: ProjectLaunchSecretValueDiagnostic[];
|
|
33
|
+
}
|
|
34
|
+
export interface ProjectLaunchSecretSyncSummaryItem {
|
|
35
|
+
provider: ProjectLaunchSecretSyncProvider;
|
|
36
|
+
scope: TreeseedConfigScope;
|
|
37
|
+
target: ProjectLaunchSecretSyncTargetKind;
|
|
38
|
+
env: string;
|
|
39
|
+
requirementKey: string;
|
|
40
|
+
requirementKind: ProjectLaunchRequirementKind;
|
|
41
|
+
sensitivity: string;
|
|
42
|
+
status: ProjectLaunchSecretSyncStatus;
|
|
43
|
+
error?: {
|
|
44
|
+
message: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface ProjectLaunchSecretSyncProviderSummary {
|
|
48
|
+
provider: ProjectLaunchSecretSyncProvider;
|
|
49
|
+
scope: TreeseedConfigScope;
|
|
50
|
+
entryIds: string[];
|
|
51
|
+
status: Exclude<ProjectLaunchSecretSyncProviderStatus, 'running'>;
|
|
52
|
+
error?: {
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface ProjectLaunchSecretSyncProgressEvent {
|
|
57
|
+
provider: ProjectLaunchSecretSyncProvider;
|
|
58
|
+
scope: TreeseedConfigScope;
|
|
59
|
+
status: ProjectLaunchSecretSyncProviderStatus;
|
|
60
|
+
entryIds: string[];
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ProjectLaunchSecretSyncResult {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
items: ProjectLaunchSecretSyncSummaryItem[];
|
|
66
|
+
providers: ProjectLaunchSecretSyncProviderSummary[];
|
|
67
|
+
diagnostics: ProjectLaunchSecretValueDiagnostic[];
|
|
68
|
+
}
|
|
69
|
+
export interface ProjectLaunchSecretSyncAdapters {
|
|
70
|
+
github?: typeof syncTreeseedGitHubEnvironment;
|
|
71
|
+
cloudflare?: typeof syncTreeseedCloudflareEnvironment;
|
|
72
|
+
railway?: typeof syncTreeseedRailwayEnvironment;
|
|
73
|
+
}
|
|
74
|
+
export interface ResolveProjectLaunchSecretValueOverlayOptions {
|
|
75
|
+
hostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
|
|
76
|
+
secretDeploymentPlan?: {
|
|
77
|
+
items?: ProjectLaunchSecretDeploymentPlanItem[] | null;
|
|
78
|
+
} | null;
|
|
79
|
+
valuesOverlay?: Record<string, string | undefined> | null;
|
|
80
|
+
valuesByScope?: Partial<Record<TreeseedConfigScope, Record<string, string | undefined> | null>> | null;
|
|
81
|
+
processEnv?: Record<string, string | undefined>;
|
|
82
|
+
scopes?: TreeseedConfigScope[];
|
|
83
|
+
}
|
|
84
|
+
export interface SyncProjectLaunchHostBindingSecretsOptions extends ResolveProjectLaunchSecretValueOverlayOptions {
|
|
85
|
+
projectRoot: string;
|
|
86
|
+
repository?: string | null;
|
|
87
|
+
dryRun?: boolean;
|
|
88
|
+
providers?: ProjectLaunchSecretSyncProvider[];
|
|
89
|
+
onProgress?: (event: ProjectLaunchSecretSyncProgressEvent) => void | Promise<void>;
|
|
90
|
+
adapters?: ProjectLaunchSecretSyncAdapters;
|
|
91
|
+
}
|
|
92
|
+
export declare class ProjectLaunchSecretSyncError extends Error {
|
|
93
|
+
readonly result: ProjectLaunchSecretSyncResult;
|
|
94
|
+
constructor(message: string, result: ProjectLaunchSecretSyncResult);
|
|
95
|
+
}
|
|
96
|
+
export declare function resolveProjectLaunchSecretValueOverlay(options: ResolveProjectLaunchSecretValueOverlayOptions): ProjectLaunchSecretValueOverlayResult;
|
|
97
|
+
export declare function syncProjectLaunchHostBindingSecrets(options: SyncProjectLaunchHostBindingSecretsOptions): Promise<ProjectLaunchSecretSyncResult>;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
syncTreeseedCloudflareEnvironment,
|
|
3
|
+
syncTreeseedGitHubEnvironment,
|
|
4
|
+
syncTreeseedRailwayEnvironment
|
|
5
|
+
} from "./config-runtime.js";
|
|
6
|
+
const PROVIDER_TARGETS = {
|
|
7
|
+
github: ["github-secret", "github-variable"],
|
|
8
|
+
cloudflare: ["cloudflare-secret", "cloudflare-var"],
|
|
9
|
+
railway: ["railway-secret", "railway-var"]
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_SCOPES = ["staging", "prod"];
|
|
12
|
+
const NON_PROVIDER_TARGETS = /* @__PURE__ */ new Set(["local-runtime", "local-cloudflare", "config-file"]);
|
|
13
|
+
const SECRET_TOKEN_PATTERN = /(?:gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{16,}|[A-Za-z0-9+/=]{32,})/gu;
|
|
14
|
+
class ProjectLaunchSecretSyncError extends Error {
|
|
15
|
+
result;
|
|
16
|
+
constructor(message, result) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ProjectLaunchSecretSyncError";
|
|
19
|
+
this.result = result;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function stringValue(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : "";
|
|
24
|
+
}
|
|
25
|
+
function normalizeSecretTargets(targets) {
|
|
26
|
+
const allowed = new Set(Object.values(PROVIDER_TARGETS).flat());
|
|
27
|
+
return targets.map((target) => String(target ?? "").trim()).filter((target) => allowed.has(target));
|
|
28
|
+
}
|
|
29
|
+
function normalizeScopes(items, requested) {
|
|
30
|
+
const scopes = new Set(requested?.length ? requested : DEFAULT_SCOPES);
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
for (const scope of item.scopes ?? []) {
|
|
33
|
+
if (scope === "staging" || scope === "prod") {
|
|
34
|
+
scopes.add(scope);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...scopes];
|
|
39
|
+
}
|
|
40
|
+
function valueCandidatesForSource(source) {
|
|
41
|
+
const value = String(source ?? "").trim();
|
|
42
|
+
if (!value) return [];
|
|
43
|
+
const [, suffix = ""] = /^(?:selectedHost|host)\.(?:secret|config|environment|env):(.+)$/u.exec(value) ?? [];
|
|
44
|
+
if (suffix) {
|
|
45
|
+
return [suffix, suffix.toUpperCase(), `TREESEED_${suffix.toUpperCase()}`];
|
|
46
|
+
}
|
|
47
|
+
const [, dotted = ""] = /^(?:selectedHost|host)\.([A-Za-z0-9_]+)$/u.exec(value) ?? [];
|
|
48
|
+
if (dotted) {
|
|
49
|
+
return [dotted, dotted.toUpperCase(), `TREESEED_${dotted.toUpperCase()}`];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function scopedOverlay(options, scope) {
|
|
54
|
+
return {
|
|
55
|
+
...options.processEnv ?? process.env,
|
|
56
|
+
...options.valuesOverlay ?? {},
|
|
57
|
+
...options.valuesByScope?.[scope] ?? {}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function bindingValue(binding, key) {
|
|
61
|
+
if (!binding || !key) return "";
|
|
62
|
+
const record = binding;
|
|
63
|
+
for (const containerKey of ["environmentValues", "configValues", "secrets", "secretValues", "metadata"]) {
|
|
64
|
+
const container = record[containerKey];
|
|
65
|
+
if (container && typeof container === "object") {
|
|
66
|
+
const value = stringValue(container[key]) || stringValue(container[key.toUpperCase()]);
|
|
67
|
+
if (value) return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return stringValue(record[key]) || stringValue(record[key.toUpperCase()]);
|
|
71
|
+
}
|
|
72
|
+
function resolvePlanItemValue(item, scope, options) {
|
|
73
|
+
const values = scopedOverlay(options, scope);
|
|
74
|
+
const direct = stringValue(values[item.env]);
|
|
75
|
+
if (direct) return direct;
|
|
76
|
+
const binding = item.sourceHostId ? options.hostBindings?.[item.requirementKey] : void 0;
|
|
77
|
+
for (const candidate of valueCandidatesForSource(item.source)) {
|
|
78
|
+
const overlayValue = stringValue(values[candidate]);
|
|
79
|
+
if (overlayValue) return overlayValue;
|
|
80
|
+
const hostValue = bindingValue(binding, candidate);
|
|
81
|
+
if (hostValue) return hostValue;
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
function providerForTarget(target) {
|
|
86
|
+
if (target.startsWith("github-")) return "github";
|
|
87
|
+
if (target.startsWith("cloudflare-")) return "cloudflare";
|
|
88
|
+
return "railway";
|
|
89
|
+
}
|
|
90
|
+
function redactSecretSyncMessage(message) {
|
|
91
|
+
return String(message instanceof Error ? message.message : message ?? "Secret sync failed.").replace(SECRET_TOKEN_PATTERN, "[redacted]").replace(/(token|password|secret|key)=([^,\s]+)/giu, "$1=[redacted]");
|
|
92
|
+
}
|
|
93
|
+
function resolveProjectLaunchSecretValueOverlay(options) {
|
|
94
|
+
const planItems = options.secretDeploymentPlan?.items?.filter(Boolean) ?? [];
|
|
95
|
+
const scopes = normalizeScopes(planItems, options.scopes);
|
|
96
|
+
const valuesOverlay = {};
|
|
97
|
+
const resolvedItems = [];
|
|
98
|
+
const diagnostics = [];
|
|
99
|
+
for (const item of planItems) {
|
|
100
|
+
const targets = normalizeSecretTargets(item.targets ?? []);
|
|
101
|
+
const itemScopes = (item.scopes ?? []).filter((scope) => scope === "local" || scope === "staging" || scope === "prod");
|
|
102
|
+
if (targets.length === 0) {
|
|
103
|
+
if ((item.targets ?? []).every((target) => NON_PROVIDER_TARGETS.has(String(target)))) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
code: "unsupported_target",
|
|
108
|
+
requirementKey: item.requirementKey,
|
|
109
|
+
requirementKind: item.requirementKind,
|
|
110
|
+
env: item.env,
|
|
111
|
+
source: item.source,
|
|
112
|
+
targets: item.targets ?? [],
|
|
113
|
+
scopes: itemScopes,
|
|
114
|
+
message: `Secret deployment target is not supported for ${item.env}.`
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const activeScopes = itemScopes.filter((scope) => scopes.includes(scope));
|
|
119
|
+
let resolved = false;
|
|
120
|
+
for (const scope of activeScopes) {
|
|
121
|
+
const value = resolvePlanItemValue(item, scope, options);
|
|
122
|
+
if (value) {
|
|
123
|
+
valuesOverlay[item.env] = value;
|
|
124
|
+
resolved = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!resolved) {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
code: "missing_value",
|
|
131
|
+
requirementKey: item.requirementKey,
|
|
132
|
+
requirementKind: item.requirementKind,
|
|
133
|
+
env: item.env,
|
|
134
|
+
source: item.source,
|
|
135
|
+
targets,
|
|
136
|
+
scopes: activeScopes,
|
|
137
|
+
message: `No launch secret value was available for ${item.env}.`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
resolvedItems.push({
|
|
141
|
+
requirementKey: item.requirementKey,
|
|
142
|
+
requirementKind: item.requirementKind,
|
|
143
|
+
env: item.env,
|
|
144
|
+
source: item.source,
|
|
145
|
+
targets,
|
|
146
|
+
scopes: activeScopes,
|
|
147
|
+
sensitivity: item.sensitivity,
|
|
148
|
+
sourceHostId: item.sourceHostId ?? null,
|
|
149
|
+
resolved
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { valuesOverlay, items: resolvedItems, diagnostics };
|
|
153
|
+
}
|
|
154
|
+
function itemsForProviderScope(items, provider, scope) {
|
|
155
|
+
const targets = new Set(PROVIDER_TARGETS[provider]);
|
|
156
|
+
return items.filter((item) => item.resolved && item.scopes.includes(scope) && item.targets.some((target) => targets.has(target)));
|
|
157
|
+
}
|
|
158
|
+
function summaryItemsFor(items, provider, scope, status, error) {
|
|
159
|
+
const targets = new Set(PROVIDER_TARGETS[provider]);
|
|
160
|
+
return items.flatMap((item) => item.targets.filter((target) => targets.has(target)).map((target) => ({
|
|
161
|
+
provider,
|
|
162
|
+
scope,
|
|
163
|
+
target,
|
|
164
|
+
env: item.env,
|
|
165
|
+
requirementKey: item.requirementKey,
|
|
166
|
+
requirementKind: item.requirementKind,
|
|
167
|
+
sensitivity: item.sensitivity,
|
|
168
|
+
status,
|
|
169
|
+
...error ? { error: { message: redactSecretSyncMessage(error) } } : {}
|
|
170
|
+
})));
|
|
171
|
+
}
|
|
172
|
+
async function syncProjectLaunchHostBindingSecrets(options) {
|
|
173
|
+
const planItems = options.secretDeploymentPlan?.items?.filter(Boolean) ?? [];
|
|
174
|
+
const scopes = normalizeScopes(planItems, options.scopes);
|
|
175
|
+
const providers = options.providers?.length ? options.providers : Object.keys(PROVIDER_TARGETS);
|
|
176
|
+
const overlay = resolveProjectLaunchSecretValueOverlay({ ...options, scopes });
|
|
177
|
+
const relevantDiagnostics = overlay.diagnostics.filter((diagnostic) => diagnostic.scopes.some((scope) => scopes.includes(scope)) && diagnostic.targets.some((target) => providers.includes(providerForTarget(target))));
|
|
178
|
+
const result = {
|
|
179
|
+
ok: relevantDiagnostics.length === 0,
|
|
180
|
+
items: [],
|
|
181
|
+
providers: [],
|
|
182
|
+
diagnostics: overlay.diagnostics
|
|
183
|
+
};
|
|
184
|
+
if (relevantDiagnostics.length > 0) {
|
|
185
|
+
for (const diagnostic of relevantDiagnostics) {
|
|
186
|
+
for (const target of normalizeSecretTargets(diagnostic.targets)) {
|
|
187
|
+
const provider = providerForTarget(target);
|
|
188
|
+
for (const scope of diagnostic.scopes.filter((entry) => scopes.includes(entry))) {
|
|
189
|
+
result.items.push({
|
|
190
|
+
provider,
|
|
191
|
+
scope,
|
|
192
|
+
target,
|
|
193
|
+
env: diagnostic.env,
|
|
194
|
+
requirementKey: diagnostic.requirementKey,
|
|
195
|
+
requirementKind: diagnostic.requirementKind,
|
|
196
|
+
sensitivity: "secret",
|
|
197
|
+
status: "failed",
|
|
198
|
+
error: { message: diagnostic.message }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw new ProjectLaunchSecretSyncError("Host-bound secret sync could not resolve every required value.", result);
|
|
204
|
+
}
|
|
205
|
+
const adapters = {
|
|
206
|
+
github: options.adapters?.github ?? syncTreeseedGitHubEnvironment,
|
|
207
|
+
cloudflare: options.adapters?.cloudflare ?? syncTreeseedCloudflareEnvironment,
|
|
208
|
+
railway: options.adapters?.railway ?? syncTreeseedRailwayEnvironment
|
|
209
|
+
};
|
|
210
|
+
for (const provider of providers) {
|
|
211
|
+
for (const scope of scopes) {
|
|
212
|
+
if (scope === "local") continue;
|
|
213
|
+
const scopedItems = itemsForProviderScope(overlay.items, provider, scope);
|
|
214
|
+
if (scopedItems.length === 0) continue;
|
|
215
|
+
const entryIds = [...new Set(scopedItems.map((item) => item.env))];
|
|
216
|
+
await options.onProgress?.({
|
|
217
|
+
provider,
|
|
218
|
+
scope,
|
|
219
|
+
entryIds,
|
|
220
|
+
status: "running",
|
|
221
|
+
message: `Syncing ${entryIds.length} host-bound ${provider} entr${entryIds.length === 1 ? "y" : "ies"} for ${scope}.`
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
const valuesOverlay = {
|
|
225
|
+
...overlay.valuesOverlay,
|
|
226
|
+
...options.valuesByScope?.[scope] ?? {}
|
|
227
|
+
};
|
|
228
|
+
if (provider === "github") {
|
|
229
|
+
await adapters.github({
|
|
230
|
+
tenantRoot: options.projectRoot,
|
|
231
|
+
scope,
|
|
232
|
+
dryRun: options.dryRun,
|
|
233
|
+
repository: options.repository ?? null,
|
|
234
|
+
valuesOverlay,
|
|
235
|
+
entryIds,
|
|
236
|
+
execution: "sequential"
|
|
237
|
+
});
|
|
238
|
+
} else if (provider === "cloudflare") {
|
|
239
|
+
adapters.cloudflare({
|
|
240
|
+
tenantRoot: options.projectRoot,
|
|
241
|
+
scope,
|
|
242
|
+
dryRun: options.dryRun,
|
|
243
|
+
valuesOverlay,
|
|
244
|
+
entryIds
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
adapters.railway({
|
|
248
|
+
tenantRoot: options.projectRoot,
|
|
249
|
+
scope,
|
|
250
|
+
dryRun: options.dryRun,
|
|
251
|
+
valuesOverlay,
|
|
252
|
+
entryIds
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
result.items.push(...summaryItemsFor(scopedItems, provider, scope, "synced"));
|
|
256
|
+
result.providers.push({ provider, scope, entryIds, status: "completed" });
|
|
257
|
+
await options.onProgress?.({
|
|
258
|
+
provider,
|
|
259
|
+
scope,
|
|
260
|
+
entryIds,
|
|
261
|
+
status: "completed",
|
|
262
|
+
message: `Synced host-bound ${provider} entries for ${scope}.`
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
result.ok = false;
|
|
266
|
+
result.items.push(...summaryItemsFor(scopedItems, provider, scope, "failed", error));
|
|
267
|
+
result.providers.push({
|
|
268
|
+
provider,
|
|
269
|
+
scope,
|
|
270
|
+
entryIds,
|
|
271
|
+
status: "failed",
|
|
272
|
+
error: { message: redactSecretSyncMessage(error) }
|
|
273
|
+
});
|
|
274
|
+
await options.onProgress?.({
|
|
275
|
+
provider,
|
|
276
|
+
scope,
|
|
277
|
+
entryIds,
|
|
278
|
+
status: "failed",
|
|
279
|
+
message: redactSecretSyncMessage(error)
|
|
280
|
+
});
|
|
281
|
+
throw new ProjectLaunchSecretSyncError(redactSecretSyncMessage(error), result);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
result.ok = result.providers.every((provider) => provider.status === "completed") && result.diagnostics.length === 0;
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
ProjectLaunchSecretSyncError,
|
|
290
|
+
resolveProjectLaunchSecretValueOverlay,
|
|
291
|
+
syncProjectLaunchHostBindingSecrets
|
|
292
|
+
};
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from "./plugins/constants.js";
|
|
10
10
|
const deployConfigFieldAliases = {
|
|
11
11
|
siteUrl: { key: "siteUrl", aliases: ["site_url"] },
|
|
12
|
-
contactEmail: { key: "contactEmail", aliases: ["contact_email"] }
|
|
12
|
+
contactEmail: { key: "contactEmail", aliases: ["contact_email"] },
|
|
13
|
+
projectRoot: { key: "projectRoot", aliases: ["project_root"] }
|
|
13
14
|
};
|
|
14
15
|
const hostingFieldAliases = {
|
|
15
16
|
kind: { key: "kind", aliases: ["kind"] },
|
|
@@ -547,6 +548,7 @@ function parseDeployConfig(raw) {
|
|
|
547
548
|
slug: expectString(parsed.slug, "slug"),
|
|
548
549
|
siteUrl: expectString(parsed.siteUrl, "siteUrl"),
|
|
549
550
|
contactEmail: expectString(parsed.contactEmail, "contactEmail"),
|
|
551
|
+
projectRoot: optionalString(parsed.projectRoot),
|
|
550
552
|
hosting: compatibilityHosting,
|
|
551
553
|
hub,
|
|
552
554
|
runtime,
|
|
@@ -630,10 +632,15 @@ function loadTreeseedDeployConfig(configPath = "treeseed.site.yaml") {
|
|
|
630
632
|
function loadTreeseedDeployConfigFromPath(resolvedConfigPath) {
|
|
631
633
|
const tenantRoot = dirname(resolvedConfigPath);
|
|
632
634
|
const parsed = parseDeployConfig(readFileSync(resolvedConfigPath, "utf8"));
|
|
635
|
+
const projectRoot = parsed.projectRoot ? resolve(tenantRoot, parsed.projectRoot) : tenantRoot;
|
|
633
636
|
Object.defineProperty(parsed, "__tenantRoot", {
|
|
634
637
|
value: tenantRoot,
|
|
635
638
|
enumerable: false
|
|
636
639
|
});
|
|
640
|
+
Object.defineProperty(parsed, "__projectRoot", {
|
|
641
|
+
value: projectRoot,
|
|
642
|
+
enumerable: false
|
|
643
|
+
});
|
|
637
644
|
Object.defineProperty(parsed, "__configPath", {
|
|
638
645
|
value: resolvedConfigPath,
|
|
639
646
|
enumerable: false
|
|
@@ -86,6 +86,9 @@ export type TreeseedEnvironmentEntry = {
|
|
|
86
86
|
localDefaultValue?: TreeseedEnvironmentValueResolver;
|
|
87
87
|
isRelevant?: (context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, purpose?: TreeseedEnvironmentPurpose) => boolean;
|
|
88
88
|
requiredWhen?: (context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, purpose?: TreeseedEnvironmentPurpose) => boolean;
|
|
89
|
+
sourceRequirement?: string;
|
|
90
|
+
sourceHostType?: string | null;
|
|
91
|
+
sourceProvider?: string | null;
|
|
89
92
|
};
|
|
90
93
|
export type TreeseedEnvironmentEntryYaml = Omit<TreeseedEnvironmentEntry, 'id' | 'defaultValue' | 'localDefaultValue' | 'isRelevant' | 'requiredWhen'> & {
|
|
91
94
|
cluster?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProjectConnection, RemoteJobStatus } from './sdk-types.ts';
|
|
1
|
+
import type { ProjectConnection, ProjectLaunchHostBindingInput, RemoteJobStatus } from './sdk-types.ts';
|
|
2
2
|
export declare const PROJECT_TEAM_CAPABILITIES: readonly ["launch_projects", "edit_direct", "manage_workstreams", "stage_releases", "publish_releases", "publish_market_listings", "manage_products", "manage_billing", "approve_remote_execution"];
|
|
3
3
|
export declare const PROJECT_JOB_STATUSES: readonly ["queued", "running", "waiting_for_approval", "failed", "completed", "rolled_back", "cancelled"];
|
|
4
4
|
export declare const WORKSTREAM_STATES: readonly ["drafting", "active_local", "verifying", "saved_remote", "in_staging", "archived"];
|
|
@@ -201,9 +201,15 @@ export interface LaunchProjectRequest {
|
|
|
201
201
|
sourceKind: 'blank' | 'template' | 'knowledge_pack';
|
|
202
202
|
sourceRef?: string | null;
|
|
203
203
|
hostingMode: 'managed' | 'hybrid' | 'self_hosted';
|
|
204
|
+
repositoryHostId?: string | null;
|
|
205
|
+
repositoryHostConfig?: Record<string, unknown> | null;
|
|
204
206
|
cloudflareHostId?: string | null;
|
|
205
207
|
cloudflareHostMode?: 'team_owned' | 'treeseed_managed' | null;
|
|
206
208
|
cloudflareHostConfig?: Record<string, unknown> | null;
|
|
209
|
+
emailHostId?: string | null;
|
|
210
|
+
emailHostMode?: 'team_owned' | 'treeseed_managed' | null;
|
|
211
|
+
emailHostConfig?: Record<string, unknown> | null;
|
|
212
|
+
hostBindings?: Record<string, ProjectLaunchHostBindingInput>;
|
|
207
213
|
targetEnvironments?: Array<'local' | 'staging' | 'prod'>;
|
|
208
214
|
publicSite?: boolean;
|
|
209
215
|
repoProvider?: 'github';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TreeseedObservedUnitState, TreeseedReconcilePlan, TreeseedReconcileResult, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitVerificationResult } from './contracts.ts';
|
|
2
2
|
import { type TreeseedRunnableBootstrapSystem } from './bootstrap-systems.ts';
|
|
3
|
+
import { type TreeseedTimingEntry } from '../timing.ts';
|
|
3
4
|
export declare function observeTreeseedUnits({ tenantRoot, target, env, systems, write, }: {
|
|
4
5
|
tenantRoot: string;
|
|
5
6
|
target: TreeseedReconcileTarget;
|
|
@@ -417,6 +418,7 @@ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, syste
|
|
|
417
418
|
plans: TreeseedReconcilePlan[];
|
|
418
419
|
results: TreeseedReconcileResult[];
|
|
419
420
|
state: TreeseedReconcileStateRecord;
|
|
421
|
+
timings: TreeseedTimingEntry[];
|
|
420
422
|
}>;
|
|
421
423
|
export declare function destroyTreeseedTargetUnits({ tenantRoot, target, env, write, }: {
|
|
422
424
|
tenantRoot: string;
|
package/dist/reconcile/engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { deriveTreeseedDesiredUnits } from "./desired-state.js";
|
|
|
3
3
|
import { ensureTreeseedPersistedUnitState, desiredUnitSpecHash, loadTreeseedReconcileState, updateTreeseedPersistedUnitState, writeTreeseedReconcileState } from "./state.js";
|
|
4
4
|
import { reverseTopologicallySortedUnits, topologicallySortDesiredUnits } from "./units.js";
|
|
5
5
|
import { filterTreeseedDesiredUnitsByBootstrapSystems } from "./bootstrap-systems.js";
|
|
6
|
+
import { elapsedMs, formatDurationMs } from "../timing.js";
|
|
6
7
|
function nowIso() {
|
|
7
8
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
8
9
|
}
|
|
@@ -209,7 +210,9 @@ async function reconcileTreeseedTarget({
|
|
|
209
210
|
const context = createRunContext(tenantRoot, target, env, write);
|
|
210
211
|
const results = [];
|
|
211
212
|
const verificationMap = /* @__PURE__ */ new Map();
|
|
213
|
+
const timingEntries = [];
|
|
212
214
|
context.session.set("treeseed:verification-results", verificationMap);
|
|
215
|
+
context.session.set("treeseed:timings", timingEntries);
|
|
213
216
|
const planByUnitId = new Map(planned.plans.map((plan) => [plan.unit.unitId, plan]));
|
|
214
217
|
let persistChain = Promise.resolve();
|
|
215
218
|
const persistVerifiedResult = async (persisted, verifiedResult) => {
|
|
@@ -221,20 +224,43 @@ async function reconcileTreeseedTarget({
|
|
|
221
224
|
};
|
|
222
225
|
await runByDependencyLevel(topologicallySortDesiredUnits(planned.units), async (unit) => {
|
|
223
226
|
const plan = planByUnitId.get(unit.unitId);
|
|
224
|
-
|
|
227
|
+
const unitTiming = {
|
|
228
|
+
name: `reconcile:${plan.unit.provider}:${plan.unit.unitType}:${plan.unit.logicalName}`,
|
|
229
|
+
durationMs: 0,
|
|
230
|
+
status: "running",
|
|
231
|
+
children: [],
|
|
232
|
+
metadata: { unitId: plan.unit.unitId, action: plan.diff.action }
|
|
233
|
+
};
|
|
234
|
+
const unitStartMs = performance.now();
|
|
235
|
+
timingEntries.push(unitTiming);
|
|
236
|
+
write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
|
|
225
237
|
const adapter = registry.get(plan.unit.unitType, plan.unit.provider);
|
|
226
238
|
const persisted = ensureTreeseedPersistedUnitState(planned.state, plan.unit);
|
|
227
239
|
try {
|
|
240
|
+
const stageStartMs = performance.now();
|
|
228
241
|
await Promise.resolve(adapter.validate?.({
|
|
229
242
|
context,
|
|
230
243
|
unit: plan.unit,
|
|
231
244
|
persistedState: persisted
|
|
232
245
|
}));
|
|
246
|
+
unitTiming.children?.push({
|
|
247
|
+
name: `${unitTiming.name}:validate`,
|
|
248
|
+
durationMs: elapsedMs(stageStartMs),
|
|
249
|
+
status: "success"
|
|
250
|
+
});
|
|
233
251
|
} catch (error) {
|
|
252
|
+
unitTiming.children?.push({
|
|
253
|
+
name: `${unitTiming.name}:validate`,
|
|
254
|
+
durationMs: elapsedMs(unitStartMs),
|
|
255
|
+
status: "failed"
|
|
256
|
+
});
|
|
257
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
258
|
+
unitTiming.status = "failed";
|
|
234
259
|
wrapAdapterFailure("validate", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
235
260
|
}
|
|
236
261
|
let result;
|
|
237
262
|
try {
|
|
263
|
+
const stageStartMs = performance.now();
|
|
238
264
|
result = await Promise.resolve(adapter.reconcile({
|
|
239
265
|
context,
|
|
240
266
|
unit: plan.unit,
|
|
@@ -242,10 +268,22 @@ async function reconcileTreeseedTarget({
|
|
|
242
268
|
observed: plan.observed,
|
|
243
269
|
diff: plan.diff
|
|
244
270
|
}));
|
|
271
|
+
unitTiming.children?.push({
|
|
272
|
+
name: `${unitTiming.name}:reconcile`,
|
|
273
|
+
durationMs: elapsedMs(stageStartMs),
|
|
274
|
+
status: "success"
|
|
275
|
+
});
|
|
245
276
|
} catch (error) {
|
|
277
|
+
unitTiming.children?.push({
|
|
278
|
+
name: `${unitTiming.name}:reconcile`,
|
|
279
|
+
durationMs: elapsedMs(unitStartMs),
|
|
280
|
+
status: "failed"
|
|
281
|
+
});
|
|
282
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
283
|
+
unitTiming.status = "failed";
|
|
246
284
|
wrapAdapterFailure("reconcile", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
247
285
|
}
|
|
248
|
-
write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType}...`);
|
|
286
|
+
write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
|
|
249
287
|
const postconditions = await Promise.resolve(adapter.requiredPostconditions?.({
|
|
250
288
|
context,
|
|
251
289
|
unit: plan.unit,
|
|
@@ -253,6 +291,7 @@ async function reconcileTreeseedTarget({
|
|
|
253
291
|
}) ?? []);
|
|
254
292
|
let verification;
|
|
255
293
|
try {
|
|
294
|
+
const stageStartMs = performance.now();
|
|
256
295
|
verification = await Promise.resolve(adapter.verify({
|
|
257
296
|
context,
|
|
258
297
|
unit: plan.unit,
|
|
@@ -262,7 +301,19 @@ async function reconcileTreeseedTarget({
|
|
|
262
301
|
result,
|
|
263
302
|
postconditions
|
|
264
303
|
}));
|
|
304
|
+
unitTiming.children?.push({
|
|
305
|
+
name: `${unitTiming.name}:verify`,
|
|
306
|
+
durationMs: elapsedMs(stageStartMs),
|
|
307
|
+
status: verification.verified ? "success" : "failed"
|
|
308
|
+
});
|
|
265
309
|
} catch (error) {
|
|
310
|
+
unitTiming.children?.push({
|
|
311
|
+
name: `${unitTiming.name}:verify`,
|
|
312
|
+
durationMs: elapsedMs(unitStartMs),
|
|
313
|
+
status: "failed"
|
|
314
|
+
});
|
|
315
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
316
|
+
unitTiming.status = "failed";
|
|
266
317
|
wrapAdapterFailure("verify", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
267
318
|
}
|
|
268
319
|
const verifiedResult = {
|
|
@@ -274,6 +325,9 @@ async function reconcileTreeseedTarget({
|
|
|
274
325
|
]
|
|
275
326
|
};
|
|
276
327
|
verificationMap.set(plan.unit.unitId, verification);
|
|
328
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
329
|
+
unitTiming.status = verification.verified ? "success" : "failed";
|
|
330
|
+
write?.(`Finished ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName}) in ${formatDurationMs(unitTiming.durationMs)}.`);
|
|
277
331
|
if (!verification.verified) {
|
|
278
332
|
await persistVerifiedResult(persisted, verifiedResult);
|
|
279
333
|
throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
|
|
@@ -287,7 +341,8 @@ async function reconcileTreeseedTarget({
|
|
|
287
341
|
units: planned.units,
|
|
288
342
|
plans: planned.plans,
|
|
289
343
|
results,
|
|
290
|
-
state: planned.state
|
|
344
|
+
state: planned.state,
|
|
345
|
+
timings: timingEntries
|
|
291
346
|
};
|
|
292
347
|
}
|
|
293
348
|
async function destroyTreeseedTargetUnits({
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { scaffoldTemplateProject } from '../operations/services/template-registry.js';
|
|
4
|
+
import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from '../sdk-types.js';
|
|
4
5
|
function parseArgs(argv) {
|
|
5
6
|
const args = {
|
|
6
7
|
target: null,
|
|
7
|
-
template:
|
|
8
|
+
template: TREESEED_DEFAULT_STARTER_TEMPLATE_ID,
|
|
8
9
|
name: null,
|
|
9
10
|
slug: null,
|
|
10
11
|
siteUrl: null,
|
|
@@ -59,7 +60,7 @@ console.log(`Created Treeseed tenant from ${definition.id} at ${targetRoot}`);
|
|
|
59
60
|
console.log('Next steps:');
|
|
60
61
|
console.log(` cd ${options.target}`);
|
|
61
62
|
console.log(' npm install');
|
|
62
|
-
console.log(
|
|
63
|
+
console.log(` treeseed template show ${options.template}`);
|
|
63
64
|
console.log(' treeseed sync --check');
|
|
64
65
|
console.log(' treeseed config --environment local');
|
|
65
66
|
console.log(' treeseed dev');
|
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { corePackageRoot, packageRoot, packageScriptPath, sdkPackageRoot } from '../operations/services/runtime-tools.js';
|
|
7
7
|
import { listTemplateProducts, validateTemplateProduct } from '../operations/services/template-registry.js';
|
|
8
|
+
import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from '../sdk-types.js';
|
|
8
9
|
const npmCacheDir = process.env.TREESEED_SCAFFOLD_NPM_CACHE_DIR
|
|
9
10
|
? resolve(process.env.TREESEED_SCAFFOLD_NPM_CACHE_DIR)
|
|
10
11
|
: resolve(tmpdir(), 'treeseed-npm-cache');
|
|
@@ -204,7 +205,7 @@ async function scaffoldSite(siteRoot) {
|
|
|
204
205
|
for (const definition of await listTemplateProducts({ writeWarning: (message) => console.warn(message) })) {
|
|
205
206
|
await validateTemplateProduct(definition, { writeWarning: (message) => console.warn(message) });
|
|
206
207
|
}
|
|
207
|
-
runStep(process.execPath, [packageScriptPath('scaffold-site'), siteRoot, '--template',
|
|
208
|
+
runStep(process.execPath, [packageScriptPath('scaffold-site'), siteRoot, '--template', TREESEED_DEFAULT_STARTER_TEMPLATE_ID, '--name', 'Smoke Site', '--site-url', 'https://smoke.example.com', '--contact-email', 'hello@example.com']);
|
|
208
209
|
}
|
|
209
210
|
function installScaffold(siteRoot, { coreTarballPath, sdkTarballPath, cliTarballPath }) {
|
|
210
211
|
if (coreTarballPath && sdkTarballPath && cliTarballPath) {
|