@treeseed/sdk 0.6.2 → 0.6.3
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/operations/services/config-runtime.js +8 -5
- package/dist/operations/services/github-api.d.ts +2 -1
- package/dist/operations/services/github-api.js +71 -2
- package/dist/operations/services/project-platform.js +38 -6
- package/dist/platform/env.yaml +3 -0
- package/dist/platform/environment.d.ts +4 -0
- package/dist/platform/environment.js +3 -0
- package/package.json +1 -1
|
@@ -1893,10 +1893,14 @@ async function syncTreeseedGitHubEnvironment({
|
|
|
1893
1893
|
} : {};
|
|
1894
1894
|
const githubClient = createGitHubApiClient({ env: ghEnv });
|
|
1895
1895
|
const environment = scope === "prod" ? "production" : scope;
|
|
1896
|
+
const deploymentBranch = scope === "prod" ? PRODUCTION_BRANCH : scope === "staging" ? STAGING_BRANCH : null;
|
|
1896
1897
|
const progress = (message, stream = "stdout") => onProgress?.(message, stream);
|
|
1897
1898
|
if (!dryRun) {
|
|
1898
1899
|
progress(`[${scope}][github][environment] Ensuring GitHub environment ${environment} exists...`);
|
|
1899
|
-
await ensureGitHubActionsEnvironment(repository, environment, {
|
|
1900
|
+
await ensureGitHubActionsEnvironment(repository, environment, {
|
|
1901
|
+
client: githubClient,
|
|
1902
|
+
branchName: deploymentBranch
|
|
1903
|
+
});
|
|
1900
1904
|
}
|
|
1901
1905
|
progress(`[${scope}][github][sync] Loading existing GitHub secrets and variables...`);
|
|
1902
1906
|
const [secretNames, variableNames] = dryRun ? [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()] : await Promise.all([
|
|
@@ -1913,10 +1917,9 @@ async function syncTreeseedGitHubEnvironment({
|
|
|
1913
1917
|
if (!value) {
|
|
1914
1918
|
continue;
|
|
1915
1919
|
}
|
|
1916
|
-
if (entry.
|
|
1920
|
+
if (entry.sensitivity === "secret") {
|
|
1917
1921
|
items.push({ kind: "secret", name: entry.id, value, existed: secretNames.has(entry.id) });
|
|
1918
|
-
}
|
|
1919
|
-
if (entry.targets.includes("github-variable")) {
|
|
1922
|
+
} else {
|
|
1920
1923
|
items.push({ kind: "variable", name: entry.id, value, existed: variableNames.has(entry.id) });
|
|
1921
1924
|
}
|
|
1922
1925
|
}
|
|
@@ -2245,7 +2248,7 @@ function configGroupRank(group) {
|
|
|
2245
2248
|
}
|
|
2246
2249
|
function listRelevantTreeseedConfigEntries(registry, scope) {
|
|
2247
2250
|
return registry.entries.filter(
|
|
2248
|
-
(entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
|
|
2251
|
+
(entry) => entry.visibility !== "system" && entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
|
|
2249
2252
|
).sort((left, right) => {
|
|
2250
2253
|
const leftRequired = isTreeseedEnvironmentEntryRequired(left, registry.context, scope, "config");
|
|
2251
2254
|
const rightRequired = isTreeseedEnvironmentEntryRequired(right, registry.context, scope, "config");
|
|
@@ -66,8 +66,9 @@ export declare function listGitHubRepositoryVariableNames(repository: string | {
|
|
|
66
66
|
export declare function ensureGitHubActionsEnvironment(repository: string | {
|
|
67
67
|
owner: string;
|
|
68
68
|
name: string;
|
|
69
|
-
}, environmentName: string, { client }?: {
|
|
69
|
+
}, environmentName: string, { client, branchName, }?: {
|
|
70
70
|
client?: GitHubApiClient;
|
|
71
|
+
branchName?: string | null;
|
|
71
72
|
}): Promise<{
|
|
72
73
|
repository: string;
|
|
73
74
|
environment: string;
|
|
@@ -220,19 +220,88 @@ async function listGitHubRepositoryVariableNames(repository, { client = createGi
|
|
|
220
220
|
throw normalizeGitHubApiError(error, `Unable to list GitHub variables for ${owner}/${name}`);
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
async function ensureGitHubActionsEnvironment(repository, environmentName, {
|
|
223
|
+
async function ensureGitHubActionsEnvironment(repository, environmentName, {
|
|
224
|
+
client = createGitHubApiClient(),
|
|
225
|
+
branchName
|
|
226
|
+
} = {}) {
|
|
224
227
|
const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
225
228
|
try {
|
|
226
229
|
await withGitHubApiRetries(() => client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
|
|
227
230
|
owner,
|
|
228
231
|
repo,
|
|
229
|
-
environment_name: environmentName
|
|
232
|
+
environment_name: environmentName,
|
|
233
|
+
...branchName ? {
|
|
234
|
+
deployment_branch_policy: {
|
|
235
|
+
protected_branches: false,
|
|
236
|
+
custom_branch_policies: true
|
|
237
|
+
}
|
|
238
|
+
} : {}
|
|
230
239
|
}));
|
|
240
|
+
if (branchName) {
|
|
241
|
+
await ensureGitHubEnvironmentBranchPolicy(client, {
|
|
242
|
+
owner,
|
|
243
|
+
repo,
|
|
244
|
+
environmentName,
|
|
245
|
+
branchName
|
|
246
|
+
});
|
|
247
|
+
}
|
|
231
248
|
return { repository: `${owner}/${repo}`, environment: environmentName };
|
|
232
249
|
} catch (error) {
|
|
233
250
|
throw normalizeGitHubApiError(error, `Unable to ensure GitHub environment ${environmentName} for ${owner}/${repo}`);
|
|
234
251
|
}
|
|
235
252
|
}
|
|
253
|
+
async function ensureGitHubEnvironmentBranchPolicy(client, {
|
|
254
|
+
owner,
|
|
255
|
+
repo,
|
|
256
|
+
environmentName,
|
|
257
|
+
branchName
|
|
258
|
+
}) {
|
|
259
|
+
const response = await withGitHubApiRetries(() => client.request(
|
|
260
|
+
"GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies",
|
|
261
|
+
{
|
|
262
|
+
owner,
|
|
263
|
+
repo,
|
|
264
|
+
environment_name: environmentName,
|
|
265
|
+
per_page: 100
|
|
266
|
+
}
|
|
267
|
+
));
|
|
268
|
+
const policies = Array.isArray(response.data?.branch_policies) ? response.data.branch_policies : [];
|
|
269
|
+
const desired = policies.find((policy) => policy.name === branchName && (policy.type ?? "branch") === "branch");
|
|
270
|
+
for (const policy of policies) {
|
|
271
|
+
if (!policy.id || policy.name === branchName && (policy.type ?? "branch") === "branch") {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
await withGitHubApiRetries(() => client.request(
|
|
275
|
+
"DELETE /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}",
|
|
276
|
+
{
|
|
277
|
+
owner,
|
|
278
|
+
repo,
|
|
279
|
+
environment_name: environmentName,
|
|
280
|
+
branch_policy_id: policy.id
|
|
281
|
+
}
|
|
282
|
+
));
|
|
283
|
+
}
|
|
284
|
+
if (desired) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await withGitHubApiRetries(() => client.request(
|
|
289
|
+
"POST /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies",
|
|
290
|
+
{
|
|
291
|
+
owner,
|
|
292
|
+
repo,
|
|
293
|
+
environment_name: environmentName,
|
|
294
|
+
name: branchName,
|
|
295
|
+
type: "branch"
|
|
296
|
+
}
|
|
297
|
+
));
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error && typeof error === "object" && error.status === 303) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
236
305
|
async function paginateGitHubEnvironmentNames(client, route, params) {
|
|
237
306
|
const paginate = client.paginate;
|
|
238
307
|
return await paginateNames(() => paginate(route, {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
signEditorialPreviewToken
|
|
13
13
|
} from "../../platform/published-content.js";
|
|
14
14
|
import { createPublishedContentPipeline } from "../../platform/published-content-pipeline.js";
|
|
15
|
-
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
|
|
15
|
+
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget, resolveTreeseedBootstrapSelection } from "../../reconcile/index.js";
|
|
16
16
|
import { loadTreeseedManifest } from "../../platform/tenant-config.js";
|
|
17
17
|
import { applyTreeseedEnvironmentToProcess } from "./config-runtime.js";
|
|
18
18
|
import {
|
|
@@ -40,6 +40,7 @@ import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "./ru
|
|
|
40
40
|
import { CloudflareQueuePullClient, CloudflareQueuePushClient } from "../../remote.js";
|
|
41
41
|
import { runPrefixedCommand, runTreeseedBootstrapDag, sleep, writeTreeseedBootstrapLine } from "./bootstrap-runner.js";
|
|
42
42
|
import { runTenantDeployPreflight } from "./save-deploy-preflight.js";
|
|
43
|
+
const PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS = ["data", "web", "api", "agents"];
|
|
43
44
|
function stableHash(value) {
|
|
44
45
|
return createHash("sha256").update(value).digest("hex");
|
|
45
46
|
}
|
|
@@ -90,6 +91,30 @@ function runNodeScript(tenantRoot, scriptName, scriptArgs = []) {
|
|
|
90
91
|
throw new Error(`${scriptName} failed.`);
|
|
91
92
|
}
|
|
92
93
|
}
|
|
94
|
+
function resolveProjectPlatformBootstrapSystems(options, siteConfig = loadCliDeployConfig(options.tenantRoot)) {
|
|
95
|
+
if (options.bootstrapSystems && options.bootstrapSystems.length > 0) {
|
|
96
|
+
return [...options.bootstrapSystems];
|
|
97
|
+
}
|
|
98
|
+
const selection = resolveTreeseedBootstrapSelection({
|
|
99
|
+
deployConfig: siteConfig,
|
|
100
|
+
env: { ...process.env, ...options.env ?? {} },
|
|
101
|
+
systems: PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS,
|
|
102
|
+
skipUnavailable: true
|
|
103
|
+
});
|
|
104
|
+
for (const skipped of selection.skipped) {
|
|
105
|
+
writeTreeseedBootstrapLine(
|
|
106
|
+
options.write,
|
|
107
|
+
{
|
|
108
|
+
scope: options.scope,
|
|
109
|
+
system: skipped.system,
|
|
110
|
+
task: "availability",
|
|
111
|
+
stage: "skip"
|
|
112
|
+
},
|
|
113
|
+
skipped.reason
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return selection.runnable.filter((system) => system !== "github");
|
|
117
|
+
}
|
|
93
118
|
function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
|
|
94
119
|
const result = spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
|
|
95
120
|
cwd: tenantRoot,
|
|
@@ -818,18 +843,24 @@ async function provisionProjectPlatform(options) {
|
|
|
818
843
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
819
844
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
820
845
|
const siteConfig = loadCliDeployConfig(options.tenantRoot);
|
|
846
|
+
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
|
|
847
|
+
const selectedSystems = new Set(bootstrapSystems);
|
|
848
|
+
const env = { ...process.env, ...options.env ?? {} };
|
|
821
849
|
const summary = await reconcileTreeseedTarget({
|
|
822
850
|
tenantRoot: options.tenantRoot,
|
|
823
851
|
target,
|
|
824
|
-
env
|
|
852
|
+
env,
|
|
853
|
+
systems: bootstrapSystems
|
|
825
854
|
});
|
|
826
855
|
const verification = await collectTreeseedReconcileStatus({
|
|
827
856
|
tenantRoot: options.tenantRoot,
|
|
828
857
|
target,
|
|
829
|
-
env
|
|
858
|
+
env,
|
|
859
|
+
systems: bootstrapSystems
|
|
830
860
|
});
|
|
831
861
|
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
832
|
-
const
|
|
862
|
+
const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
|
|
863
|
+
const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
|
|
833
864
|
const railwaySchedules = [];
|
|
834
865
|
const railwayScheduleVerification = {
|
|
835
866
|
ok: true,
|
|
@@ -969,7 +1000,8 @@ async function deployProjectPlatform(options) {
|
|
|
969
1000
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
970
1001
|
const commitSha = currentCommit(options.tenantRoot);
|
|
971
1002
|
const branchName = currentRef(options.tenantRoot);
|
|
972
|
-
const
|
|
1003
|
+
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options);
|
|
1004
|
+
const selectedSystems = new Set(bootstrapSystems);
|
|
973
1005
|
const execution = options.bootstrapExecution ?? "parallel";
|
|
974
1006
|
const write = options.write;
|
|
975
1007
|
const env = { ...process.env, ...options.env ?? {} };
|
|
@@ -983,7 +1015,7 @@ async function deployProjectPlatform(options) {
|
|
|
983
1015
|
metadata: { scope: options.scope }
|
|
984
1016
|
});
|
|
985
1017
|
if (!options.skipProvision) {
|
|
986
|
-
await provisionProjectPlatform({ ...options, reporter });
|
|
1018
|
+
await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
987
1019
|
}
|
|
988
1020
|
const nodes = [];
|
|
989
1021
|
let cloudflareContext = null;
|
package/dist/platform/env.yaml
CHANGED
|
@@ -748,6 +748,7 @@ entries:
|
|
|
748
748
|
TREESEED_RAILWAY_PROJECT_ID:
|
|
749
749
|
label: Railway project ID
|
|
750
750
|
group: hosting
|
|
751
|
+
visibility: system
|
|
751
752
|
description: Railway project identifier used by runtime scaling and reconciliation helpers for the active environment.
|
|
752
753
|
howToGet: Copy the project ID from Railway when automatic worker scaling is enabled.
|
|
753
754
|
sensitivity: plain
|
|
@@ -770,6 +771,7 @@ entries:
|
|
|
770
771
|
TREESEED_RAILWAY_ENVIRONMENT_ID:
|
|
771
772
|
label: Railway environment ID
|
|
772
773
|
group: hosting
|
|
774
|
+
visibility: system
|
|
773
775
|
description: Railway environment identifier used by the runtime scaler when adjusting worker replicas.
|
|
774
776
|
howToGet: Copy the environment ID from Railway for the matching staging or production environment.
|
|
775
777
|
sensitivity: plain
|
|
@@ -792,6 +794,7 @@ entries:
|
|
|
792
794
|
TREESEED_RAILWAY_WORKER_SERVICE_ID:
|
|
793
795
|
label: Railway worker service ID
|
|
794
796
|
group: hosting
|
|
797
|
+
visibility: system
|
|
795
798
|
description: Railway service identifier for the scalable worker pool that the manager adjusts at runtime.
|
|
796
799
|
howToGet: Copy the worker service ID from Railway after the hosted project environment is provisioned.
|
|
797
800
|
sensitivity: plain
|
|
@@ -7,6 +7,7 @@ export declare const TREESEED_ENVIRONMENT_PURPOSES: readonly ["dev", "save", "de
|
|
|
7
7
|
export declare const TREESEED_ENVIRONMENT_SENSITIVITY: readonly ["secret", "plain", "derived"];
|
|
8
8
|
export declare const TREESEED_ENVIRONMENT_STORAGE: readonly ["scoped", "shared"];
|
|
9
9
|
export declare const TREESEED_CONFIG_STARTUP_PROFILES: readonly ["core", "optional", "advanced"];
|
|
10
|
+
export declare const TREESEED_ENVIRONMENT_VISIBILITY: readonly ["user", "system"];
|
|
10
11
|
export type TreeseedEnvironmentScope = (typeof TREESEED_ENVIRONMENT_SCOPES)[number];
|
|
11
12
|
export type TreeseedEnvironmentRequirement = (typeof TREESEED_ENVIRONMENT_REQUIREMENTS)[number];
|
|
12
13
|
export type TreeseedEnvironmentTarget = (typeof TREESEED_ENVIRONMENT_TARGETS)[number];
|
|
@@ -14,6 +15,7 @@ export type TreeseedEnvironmentPurpose = (typeof TREESEED_ENVIRONMENT_PURPOSES)[
|
|
|
14
15
|
export type TreeseedEnvironmentSensitivity = (typeof TREESEED_ENVIRONMENT_SENSITIVITY)[number];
|
|
15
16
|
export type TreeseedEnvironmentStorage = (typeof TREESEED_ENVIRONMENT_STORAGE)[number];
|
|
16
17
|
export type TreeseedConfigStartupProfile = (typeof TREESEED_CONFIG_STARTUP_PROFILES)[number];
|
|
18
|
+
export type TreeseedEnvironmentVisibility = (typeof TREESEED_ENVIRONMENT_VISIBILITY)[number];
|
|
17
19
|
export type TreeseedEnvironmentValidation = {
|
|
18
20
|
kind: 'string' | 'nonempty' | 'url' | 'email';
|
|
19
21
|
minLength?: number;
|
|
@@ -68,6 +70,7 @@ export type TreeseedEnvironmentEntry = {
|
|
|
68
70
|
cluster?: string;
|
|
69
71
|
onboardingFeature?: string;
|
|
70
72
|
startupProfile?: TreeseedConfigStartupProfile;
|
|
73
|
+
visibility?: TreeseedEnvironmentVisibility;
|
|
71
74
|
description: string;
|
|
72
75
|
howToGet: string;
|
|
73
76
|
sensitivity: TreeseedEnvironmentSensitivity;
|
|
@@ -87,6 +90,7 @@ export type TreeseedEnvironmentEntryYaml = Omit<TreeseedEnvironmentEntry, 'id' |
|
|
|
87
90
|
cluster?: string;
|
|
88
91
|
onboardingFeature?: string;
|
|
89
92
|
startupProfile?: TreeseedConfigStartupProfile;
|
|
93
|
+
visibility?: TreeseedEnvironmentVisibility;
|
|
90
94
|
defaultValueRef?: string;
|
|
91
95
|
localDefaultValueRef?: string;
|
|
92
96
|
relevanceRef?: string;
|
|
@@ -24,6 +24,7 @@ const TREESEED_ENVIRONMENT_PURPOSES = ["dev", "save", "deploy", "destroy", "conf
|
|
|
24
24
|
const TREESEED_ENVIRONMENT_SENSITIVITY = ["secret", "plain", "derived"];
|
|
25
25
|
const TREESEED_ENVIRONMENT_STORAGE = ["scoped", "shared"];
|
|
26
26
|
const TREESEED_CONFIG_STARTUP_PROFILES = ["core", "optional", "advanced"];
|
|
27
|
+
const TREESEED_ENVIRONMENT_VISIBILITY = ["user", "system"];
|
|
27
28
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
function resolveCoreEnvironmentPath() {
|
|
29
30
|
const candidates = [
|
|
@@ -367,6 +368,7 @@ function materializeEntry(id, entry) {
|
|
|
367
368
|
id,
|
|
368
369
|
cluster: entry.cluster ?? `${entry.group}:${id}`,
|
|
369
370
|
onboardingFeature: entry.onboardingFeature,
|
|
371
|
+
visibility: entry.visibility ?? "user",
|
|
370
372
|
startupProfile: entry.startupProfile ?? (entry.onboardingFeature ? "optional" : entry.group === "auth" || entry.id === "TREESEED_FORM_TOKEN_SECRET" || entry.group === "local-development" ? "core" : "advanced"),
|
|
371
373
|
storage: entry.storage ?? "scoped",
|
|
372
374
|
defaultValue: resolveNamedValueResolver(entry.defaultValueRef),
|
|
@@ -578,6 +580,7 @@ export {
|
|
|
578
580
|
TREESEED_ENVIRONMENT_SENSITIVITY,
|
|
579
581
|
TREESEED_ENVIRONMENT_STORAGE,
|
|
580
582
|
TREESEED_ENVIRONMENT_TARGETS,
|
|
583
|
+
TREESEED_ENVIRONMENT_VISIBILITY,
|
|
581
584
|
getTreeseedEnvironmentSuggestedValues,
|
|
582
585
|
isTreeseedEnvironmentEntryRelevant,
|
|
583
586
|
isTreeseedEnvironmentEntryRequired,
|