@treeseed/sdk 0.8.1 → 0.8.2
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.d.ts +2 -1
- package/dist/operations/services/config-runtime.js +21 -1
- package/dist/operations/services/github-automation.d.ts +10 -3
- package/dist/operations/services/github-automation.js +20 -8
- package/dist/operations/services/hosting-audit.d.ts +67 -0
- package/dist/operations/services/hosting-audit.js +642 -0
- package/dist/operations/services/hub-launch.js +2 -2
- package/dist/operations/services/hub-provider-launch.js +4 -4
- package/dist/operations/services/managed-host-security.d.ts +13 -0
- package/dist/operations/services/managed-host-security.js +53 -0
- package/dist/platform/env.yaml +49 -0
- package/dist/reconcile/builtin-adapters.js +5 -4
- package/dist/workflow/operations.d.ts +6 -0
- package/dist/workflow/operations.js +46 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/package.json +1 -1
- package/templates/github/deploy.managed.workflow.yml +208 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TreeseedDeployConfig } from '../../platform/contracts.ts';
|
|
2
|
+
export declare const MANAGED_HOST_DIRECT_CI_OPT_IN_ENV = "TREESEED_ALLOW_MANAGED_HOST_DIRECT_CI_SECRETS";
|
|
3
|
+
export declare function allowsManagedHostDirectCiSecrets(env?: Record<string, string | undefined>): boolean;
|
|
4
|
+
export declare function isTreeseedManagedHostedProject(deployConfig: Pick<TreeseedDeployConfig, 'hosting' | 'hub' | 'runtime'> | null | undefined): boolean;
|
|
5
|
+
export declare function usesManagedHostOperationRequests(deployConfig: Pick<TreeseedDeployConfig, 'hosting' | 'hub' | 'runtime'> | null | undefined, env?: Record<string, string | undefined>): boolean;
|
|
6
|
+
export declare function filterManagedHostGitHubEnvironment(required: {
|
|
7
|
+
secrets: string[];
|
|
8
|
+
variables: string[];
|
|
9
|
+
}): {
|
|
10
|
+
secrets: never[];
|
|
11
|
+
variables: string[];
|
|
12
|
+
};
|
|
13
|
+
export declare function shouldExposeManagedHostRuntimeSecret(deployConfig: Pick<TreeseedDeployConfig, 'hosting' | 'hub' | 'runtime'> | null | undefined, _secretName: string, env?: Record<string, string | undefined>): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const MANAGED_HOST_DIRECT_CI_OPT_IN_ENV = "TREESEED_ALLOW_MANAGED_HOST_DIRECT_CI_SECRETS";
|
|
2
|
+
const SAFE_MANAGED_HOST_CI_VARIABLES = /* @__PURE__ */ new Set([
|
|
3
|
+
"TREESEED_CATALOG_MARKET_API_BASE_URLS",
|
|
4
|
+
"TREESEED_CENTRAL_MARKET_API_BASE_URL",
|
|
5
|
+
"TREESEED_HOSTING_KIND",
|
|
6
|
+
"TREESEED_HOSTING_REGISTRATION",
|
|
7
|
+
"TREESEED_HOSTING_TEAM_ID",
|
|
8
|
+
"TREESEED_MARKET_API_BASE_URL",
|
|
9
|
+
"TREESEED_PROJECT_ID"
|
|
10
|
+
]);
|
|
11
|
+
const MANAGED_HOST_FORBIDDEN_VARIABLE_PREFIXES = [
|
|
12
|
+
"CLOUDFLARE_",
|
|
13
|
+
"RAILWAY_",
|
|
14
|
+
"TREESEED_API_",
|
|
15
|
+
"TREESEED_AUTH_",
|
|
16
|
+
"TREESEED_CLOUDFLARE_",
|
|
17
|
+
"TREESEED_RAILWAY_",
|
|
18
|
+
"TREESEED_SMTP_",
|
|
19
|
+
"TREESEED_TURNSTILE_",
|
|
20
|
+
"TREESEED_WEB_"
|
|
21
|
+
];
|
|
22
|
+
function allowsManagedHostDirectCiSecrets(env = process.env) {
|
|
23
|
+
const value = env[MANAGED_HOST_DIRECT_CI_OPT_IN_ENV];
|
|
24
|
+
return value === "1" || value === "true" || value === "yes";
|
|
25
|
+
}
|
|
26
|
+
function isTreeseedManagedHostedProject(deployConfig) {
|
|
27
|
+
return deployConfig?.hosting?.kind === "hosted_project" && deployConfig?.hub?.mode === "treeseed_hosted" && deployConfig?.runtime?.mode === "treeseed_managed";
|
|
28
|
+
}
|
|
29
|
+
function usesManagedHostOperationRequests(deployConfig, env = process.env) {
|
|
30
|
+
return isTreeseedManagedHostedProject(deployConfig) && !allowsManagedHostDirectCiSecrets(env);
|
|
31
|
+
}
|
|
32
|
+
function filterManagedHostGitHubEnvironment(required) {
|
|
33
|
+
return {
|
|
34
|
+
secrets: [],
|
|
35
|
+
variables: required.variables.filter((name) => {
|
|
36
|
+
if (SAFE_MANAGED_HOST_CI_VARIABLES.has(name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return !MANAGED_HOST_FORBIDDEN_VARIABLE_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function shouldExposeManagedHostRuntimeSecret(deployConfig, _secretName, env = process.env) {
|
|
44
|
+
return !usesManagedHostOperationRequests(deployConfig, env);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
MANAGED_HOST_DIRECT_CI_OPT_IN_ENV,
|
|
48
|
+
allowsManagedHostDirectCiSecrets,
|
|
49
|
+
filterManagedHostGitHubEnvironment,
|
|
50
|
+
isTreeseedManagedHostedProject,
|
|
51
|
+
shouldExposeManagedHostRuntimeSecret,
|
|
52
|
+
usesManagedHostOperationRequests
|
|
53
|
+
};
|
package/dist/platform/env.yaml
CHANGED
|
@@ -87,6 +87,55 @@ entries:
|
|
|
87
87
|
- machine-config
|
|
88
88
|
- process-env
|
|
89
89
|
defaultValueRef: githubRepositoryVisibilityDefault
|
|
90
|
+
TREESEED_HOSTED_HUBS_GITHUB_OWNER:
|
|
91
|
+
label: Hosted repository owner
|
|
92
|
+
group: github
|
|
93
|
+
description: GitHub user or organization where TreeSeed-managed hosted hub repositories are created.
|
|
94
|
+
howToGet: Enter the GitHub organization or owner dedicated to hosted Knowledge Hub repositories. This can be different from the Market source repository owner.
|
|
95
|
+
sensitivity: plain
|
|
96
|
+
targets:
|
|
97
|
+
- local-runtime
|
|
98
|
+
- railway-var
|
|
99
|
+
scopes:
|
|
100
|
+
- staging
|
|
101
|
+
- prod
|
|
102
|
+
storage: shared
|
|
103
|
+
requirement: conditional
|
|
104
|
+
relevanceRef: marketControlPlaneEnabled
|
|
105
|
+
requiredWhenRef: marketControlPlaneEnabled
|
|
106
|
+
purposes:
|
|
107
|
+
- config
|
|
108
|
+
- deploy
|
|
109
|
+
validation:
|
|
110
|
+
kind: nonempty
|
|
111
|
+
sourcePriority:
|
|
112
|
+
- machine-config
|
|
113
|
+
- process-env
|
|
114
|
+
TREESEED_HOSTED_HUBS_GITHUB_TOKEN:
|
|
115
|
+
label: Hosted repository access token
|
|
116
|
+
group: github
|
|
117
|
+
description: GitHub token used only by TreeSeed-controlled workers to create and configure TreeSeed-managed hosted hub repositories.
|
|
118
|
+
howToGet: Create a GitHub token or app credential for the hosted hub repository owner with repository create/configure permissions, then store it here. Do not sync this value to team repositories or GitHub Actions secrets.
|
|
119
|
+
sensitivity: secret
|
|
120
|
+
targets:
|
|
121
|
+
- local-runtime
|
|
122
|
+
- railway-secret
|
|
123
|
+
scopes:
|
|
124
|
+
- staging
|
|
125
|
+
- prod
|
|
126
|
+
storage: shared
|
|
127
|
+
requirement: conditional
|
|
128
|
+
relevanceRef: marketControlPlaneEnabled
|
|
129
|
+
requiredWhenRef: marketControlPlaneEnabled
|
|
130
|
+
purposes:
|
|
131
|
+
- config
|
|
132
|
+
- deploy
|
|
133
|
+
validation:
|
|
134
|
+
kind: nonempty
|
|
135
|
+
minLength: 8
|
|
136
|
+
sourcePriority:
|
|
137
|
+
- machine-config
|
|
138
|
+
- process-env
|
|
90
139
|
CLOUDFLARE_API_TOKEN:
|
|
91
140
|
label: Cloudflare API token
|
|
92
141
|
group: auth
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
runRailway,
|
|
35
35
|
validateRailwayDeployPrerequisites
|
|
36
36
|
} from "../operations/services/railway-deploy.js";
|
|
37
|
+
import { shouldExposeManagedHostRuntimeSecret } from "../operations/services/managed-host-security.js";
|
|
37
38
|
import {
|
|
38
39
|
ensureRailwayEnvironment,
|
|
39
40
|
ensureRailwayProject,
|
|
@@ -492,7 +493,7 @@ function collectCloudflareEnvironmentSync(input) {
|
|
|
492
493
|
const value = typeof values[entry.id] === "string" ? values[entry.id] : "";
|
|
493
494
|
if (entry.targets.includes("cloudflare-secret")) {
|
|
494
495
|
const secretValue = value || (typeof generatedSecrets[entry.id] === "string" ? generatedSecrets[entry.id] : "");
|
|
495
|
-
if (secretValue) {
|
|
496
|
+
if (secretValue && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, entry.id)) {
|
|
496
497
|
secrets[entry.id] = secretValue;
|
|
497
498
|
secretNames.add(entry.id);
|
|
498
499
|
}
|
|
@@ -503,7 +504,7 @@ function collectCloudflareEnvironmentSync(input) {
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
for (const [key, value] of Object.entries(generatedSecrets)) {
|
|
506
|
-
if (typeof value === "string" && value.length > 0) {
|
|
507
|
+
if (typeof value === "string" && value.length > 0 && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, key)) {
|
|
507
508
|
secrets[key] = value;
|
|
508
509
|
secretNames.add(key);
|
|
509
510
|
}
|
|
@@ -1528,12 +1529,12 @@ function collectRailwayEnvironmentSync(input) {
|
|
|
1528
1529
|
const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
|
|
1529
1530
|
const state = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target: toDeployTarget(input.context.target) });
|
|
1530
1531
|
const secrets = Object.fromEntries(
|
|
1531
|
-
registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("railway-secret")).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0)
|
|
1532
|
+
registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("railway-secret") && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, entry.id)).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0)
|
|
1532
1533
|
);
|
|
1533
1534
|
const variables = Object.fromEntries(
|
|
1534
1535
|
registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("railway-var")).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0)
|
|
1535
1536
|
);
|
|
1536
|
-
if (typeof values.CLOUDFLARE_API_TOKEN === "string" && values.CLOUDFLARE_API_TOKEN.length > 0) {
|
|
1537
|
+
if (typeof values.CLOUDFLARE_API_TOKEN === "string" && values.CLOUDFLARE_API_TOKEN.length > 0 && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, "CLOUDFLARE_API_TOKEN")) {
|
|
1537
1538
|
secrets.CLOUDFLARE_API_TOKEN = values.CLOUDFLARE_API_TOKEN;
|
|
1538
1539
|
}
|
|
1539
1540
|
if (typeof values.CLOUDFLARE_ACCOUNT_ID === "string" && values.CLOUDFLARE_ACCOUNT_ID.length > 0) {
|
|
@@ -424,6 +424,7 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
424
424
|
cached: boolean;
|
|
425
425
|
}[];
|
|
426
426
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
427
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
427
428
|
} & {
|
|
428
429
|
finalState?: WorkflowStatePayload;
|
|
429
430
|
}>>;
|
|
@@ -566,6 +567,7 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
566
567
|
cached: boolean;
|
|
567
568
|
}[];
|
|
568
569
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
570
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
569
571
|
} & {
|
|
570
572
|
finalState?: WorkflowStatePayload;
|
|
571
573
|
}) | null;
|
|
@@ -749,6 +751,7 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
|
|
|
749
751
|
cached: boolean;
|
|
750
752
|
}[];
|
|
751
753
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
754
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
752
755
|
} & {
|
|
753
756
|
finalState?: WorkflowStatePayload;
|
|
754
757
|
}) | null;
|
|
@@ -777,6 +780,7 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
|
|
|
777
780
|
workspaceLinks: import("../operations/services/workspace-dependency-mode.ts").WorkspaceDependencyModeReport;
|
|
778
781
|
ciMode: "hosted" | "off";
|
|
779
782
|
workflowGates: any;
|
|
783
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
780
784
|
worktreeCleanup: {
|
|
781
785
|
removed: boolean;
|
|
782
786
|
reason: string;
|
|
@@ -974,6 +978,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
974
978
|
timeoutSeconds: number | null;
|
|
975
979
|
cached: boolean;
|
|
976
980
|
}[];
|
|
981
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
977
982
|
} & {
|
|
978
983
|
finalState?: WorkflowStatePayload;
|
|
979
984
|
}> | TreeseedWorkflowResult<{
|
|
@@ -1104,6 +1109,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
1104
1109
|
timeoutSeconds: number | null;
|
|
1105
1110
|
cached: boolean;
|
|
1106
1111
|
})[];
|
|
1112
|
+
hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
|
|
1107
1113
|
} & {
|
|
1108
1114
|
finalState?: WorkflowStatePayload;
|
|
1109
1115
|
}>>;
|
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
workspacePackages,
|
|
125
125
|
workspaceRoot
|
|
126
126
|
} from "../operations/services/workspace-tools.js";
|
|
127
|
+
import { runTreeseedHostingAudit } from "../operations/services/hosting-audit.js";
|
|
127
128
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
128
129
|
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, filterTreeseedDesiredUnitsByBootstrapSystems, planTreeseedReconciliation, resolveTreeseedBootstrapSelection, reconcileTreeseedTarget } from "../reconcile/index.js";
|
|
129
130
|
import {
|
|
@@ -631,6 +632,25 @@ function buildWorkflowResult(operation, cwd, payload, options = {}) {
|
|
|
631
632
|
errors: options.errors ?? []
|
|
632
633
|
};
|
|
633
634
|
}
|
|
635
|
+
async function runReadOnlyHostingAuditForWorkflow(operation, root, helpers, environment, options = { enabled: true }) {
|
|
636
|
+
if (!options.enabled) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
helpers.write(`[workflow][hosting-audit] Running read-only hosting audit for ${environment}.`);
|
|
640
|
+
const report = await runTreeseedHostingAudit({
|
|
641
|
+
tenantRoot: root,
|
|
642
|
+
environment,
|
|
643
|
+
repair: false,
|
|
644
|
+
env: helpers.context.env,
|
|
645
|
+
write: (line) => helpers.write(line)
|
|
646
|
+
});
|
|
647
|
+
if (options.strict && !report.ok) {
|
|
648
|
+
workflowError(operation, "validation_failed", `Hosting audit failed for ${report.environment}: ${report.blockers.join("\n")}`, {
|
|
649
|
+
details: { hostingAudit: report }
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return report;
|
|
653
|
+
}
|
|
634
654
|
function normalizeExecutionMode(input) {
|
|
635
655
|
return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
|
|
636
656
|
}
|
|
@@ -3025,6 +3045,13 @@ async function workflowSave(helpers, input) {
|
|
|
3025
3045
|
};
|
|
3026
3046
|
}
|
|
3027
3047
|
}
|
|
3048
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
|
|
3049
|
+
"save",
|
|
3050
|
+
root,
|
|
3051
|
+
helpers,
|
|
3052
|
+
scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
|
|
3053
|
+
{ enabled: effectiveInput.verifyDeployedResources === true, strict: true }
|
|
3054
|
+
);
|
|
3028
3055
|
const payload = {
|
|
3029
3056
|
mode: saveResult?.mode ?? mode,
|
|
3030
3057
|
branch,
|
|
@@ -3052,6 +3079,7 @@ async function workflowSave(helpers, input) {
|
|
|
3052
3079
|
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
3053
3080
|
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
3054
3081
|
releaseCandidate,
|
|
3082
|
+
hostingAudit,
|
|
3055
3083
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3056
3084
|
};
|
|
3057
3085
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
@@ -3549,6 +3577,13 @@ async function workflowStage(helpers, input) {
|
|
|
3549
3577
|
blockers: []
|
|
3550
3578
|
});
|
|
3551
3579
|
const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("stage", root, stageReleasePlan));
|
|
3580
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
|
|
3581
|
+
"stage",
|
|
3582
|
+
root,
|
|
3583
|
+
helpers,
|
|
3584
|
+
"staging",
|
|
3585
|
+
{ enabled: effectiveInput.verifyDeployedResources === true, strict: true }
|
|
3586
|
+
);
|
|
3552
3587
|
const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
|
|
3553
3588
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
3554
3589
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
@@ -3605,6 +3640,7 @@ async function workflowStage(helpers, input) {
|
|
|
3605
3640
|
workspaceLinks,
|
|
3606
3641
|
ciMode,
|
|
3607
3642
|
workflowGates: stagingWait.workflowGates,
|
|
3643
|
+
hostingAudit,
|
|
3608
3644
|
worktreeCleanup,
|
|
3609
3645
|
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3610
3646
|
managedWorktree,
|
|
@@ -3861,6 +3897,10 @@ async function workflowRelease(helpers, input) {
|
|
|
3861
3897
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3862
3898
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3863
3899
|
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
3900
|
+
const hostingAudit2 = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
|
|
3901
|
+
enabled: true,
|
|
3902
|
+
strict: false
|
|
3903
|
+
});
|
|
3864
3904
|
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
|
|
3865
3905
|
version: rootVersion,
|
|
3866
3906
|
changelog: rootRelease2?.changelog ?? null
|
|
@@ -3893,6 +3933,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3893
3933
|
workspaceLinks: workspaceLinks2,
|
|
3894
3934
|
ciMode,
|
|
3895
3935
|
workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
|
|
3936
|
+
hostingAudit: hostingAudit2,
|
|
3896
3937
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3897
3938
|
};
|
|
3898
3939
|
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
@@ -4162,6 +4203,10 @@ async function workflowRelease(helpers, input) {
|
|
|
4162
4203
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4163
4204
|
}).then((workflowGates) => ({ workflowGates })));
|
|
4164
4205
|
const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
|
|
4206
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
|
|
4207
|
+
enabled: true,
|
|
4208
|
+
strict: false
|
|
4209
|
+
});
|
|
4165
4210
|
const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
|
|
4166
4211
|
version: rootVersion,
|
|
4167
4212
|
changelog: rootRelease?.changelog ?? null,
|
|
@@ -4214,6 +4259,7 @@ async function workflowRelease(helpers, input) {
|
|
|
4214
4259
|
...packageReports.flatMap((report) => report.workflowGates),
|
|
4215
4260
|
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
4216
4261
|
],
|
|
4262
|
+
hostingAudit,
|
|
4217
4263
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
4218
4264
|
};
|
|
4219
4265
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { applyTreeseedConfigValues, applyTreeseedEnvironmentToProcess, applyTreeseedSafeRepairs, assertTreeseedCommandEnvironment, checkTreeseedProviderConnections, clearTreeseedRemoteSession, collectTreeseedConfigContext, collectTreeseedPrintEnvReport, createDefaultTreeseedMachineConfig, ensureTreeseedActVerificationTooling, ensureTreeseedSecretSessionForConfig, ensureTreeseedGitignoreEntries, getTreeseedMachineConfigPaths, loadTreeseedMachineConfig, listRelevantTreeseedConfigEntries, finalizeTreeseedConfig, inspectTreeseedKeyAgentTransportDiagnostic, inspectTreeseedPassphraseEnvDiagnostic, listDeprecatedTreeseedLocalEnvFiles, inspectTreeseedKeyAgentStatus, lockTreeseedSecretSession, migrateTreeseedMachineKeyToWrapped, resolveTreeseedMachineEnvironmentValues, resolveTreeseedLaunchEnvironment, resolveTreeseedRemoteConfig, resolveTreeseedRemoteSession, rotateTreeseedMachineKey, rotateTreeseedMachineKeyPassphrase, setTreeseedRemoteSession, TREESEED_MACHINE_KEY_PASSPHRASE_ENV, TreeseedKeyAgentError, updateTreeseedDeployConfigFeatureToggles, unlockTreeseedSecretSessionFromEnv, unlockTreeseedSecretSessionInteractive, unlockTreeseedSecretSessionWithPassphrase, withTreeseedKeyAgentAutopromptDisabled, warnDeprecatedTreeseedLocalEnvFiles, writeTreeseedMachineConfig, } from './operations/services/config-runtime.ts';
|
|
2
2
|
export { exportTreeseedCodebase } from './operations/services/export-runtime.ts';
|
|
3
|
+
export { formatTreeseedHostingAuditReport, resolveTreeseedHostingAuditTarget, runTreeseedHostingAudit, type TreeseedHostingAuditCheck, type TreeseedHostingAuditEnvironment, type TreeseedHostingAuditHostKind, type TreeseedHostingAuditReport, } from './operations/services/hosting-audit.ts';
|
|
3
4
|
export { assertDeploymentInitialized, cleanupDestroyedState, createBranchPreviewDeployTarget, createPersistentDeployTarget, deployTargetLabel, destroyCloudflareResources, ensureGeneratedWranglerConfig, finalizeDeploymentState, loadDeployState, printDeploySummary, printDestroySummary, provisionCloudflareResources, runRemoteD1Migrations, syncCloudflareSecrets, validateDeployPrerequisites, validateDestroyPrerequisites, } from './operations/services/deploy.ts';
|
|
4
5
|
export { assertCleanWorktree, assertFeatureBranch, branchExists, checkoutBranch, createDeprecatedTaskTag, createFeatureBranchFromStaging, currentManagedBranch, deleteLocalBranch, deleteRemoteBranch, ensureLocalBranchTracking, gitWorkflowRoot, listTaskBranches, mergeCurrentBranchIntoStaging, mergeStagingIntoMain, prepareReleaseBranches, PRODUCTION_BRANCH, pushBranch, remoteBranchExists, STAGING_BRANCH, syncBranchWithOrigin, waitForStagingAutomation, } from './operations/services/git-workflow.ts';
|
|
5
6
|
export { dockerIsAvailable, stopKnownMailpitContainers, type TreeseedMailpitContainer, } from './operations/services/mailpit-runtime.ts';
|
package/dist/workflow-support.js
CHANGED
|
@@ -39,6 +39,11 @@ import {
|
|
|
39
39
|
writeTreeseedMachineConfig
|
|
40
40
|
} from "./operations/services/config-runtime.js";
|
|
41
41
|
import { exportTreeseedCodebase } from "./operations/services/export-runtime.js";
|
|
42
|
+
import {
|
|
43
|
+
formatTreeseedHostingAuditReport,
|
|
44
|
+
resolveTreeseedHostingAuditTarget,
|
|
45
|
+
runTreeseedHostingAudit
|
|
46
|
+
} from "./operations/services/hosting-audit.js";
|
|
42
47
|
import {
|
|
43
48
|
assertDeploymentInitialized,
|
|
44
49
|
cleanupDestroyedState,
|
|
@@ -228,6 +233,7 @@ export {
|
|
|
228
233
|
formatMergeConflictReport,
|
|
229
234
|
formatTreeseedDependencyFailureDetails,
|
|
230
235
|
formatTreeseedDependencyReport,
|
|
236
|
+
formatTreeseedHostingAuditReport,
|
|
231
237
|
getRailwayAuthProfile,
|
|
232
238
|
getTreeseedMachineConfigPaths,
|
|
233
239
|
gitStatusPorcelain,
|
|
@@ -272,6 +278,7 @@ export {
|
|
|
272
278
|
resolveRailwayApiUrl,
|
|
273
279
|
resolveRailwayWorkspace,
|
|
274
280
|
resolveRailwayWorkspaceContext,
|
|
281
|
+
resolveTreeseedHostingAuditTarget,
|
|
275
282
|
resolveTreeseedLaunchEnvironment,
|
|
276
283
|
resolveTreeseedMachineEnvironmentValues,
|
|
277
284
|
resolveTreeseedRemoteConfig,
|
|
@@ -285,6 +292,7 @@ export {
|
|
|
285
292
|
runRemoteD1Migrations,
|
|
286
293
|
runTenantDeployPreflight,
|
|
287
294
|
runTreeseedCopilotTask,
|
|
295
|
+
runTreeseedHostingAudit,
|
|
288
296
|
runWorkspaceReleasePreflight,
|
|
289
297
|
runWorkspaceSavePreflight,
|
|
290
298
|
setTreeseedRemoteSession,
|
package/package.json
CHANGED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
name: Treeseed Managed Hosted Project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
project_id:
|
|
7
|
+
description: Treeseed project id recorded in the Market control plane
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
environment:
|
|
11
|
+
description: Target environment
|
|
12
|
+
required: true
|
|
13
|
+
default: staging
|
|
14
|
+
type: choice
|
|
15
|
+
options:
|
|
16
|
+
- staging
|
|
17
|
+
- prod
|
|
18
|
+
action_kind:
|
|
19
|
+
description: Requested managed operation
|
|
20
|
+
required: true
|
|
21
|
+
default: deploy_code
|
|
22
|
+
type: choice
|
|
23
|
+
options:
|
|
24
|
+
- provision
|
|
25
|
+
- deploy_code
|
|
26
|
+
- publish_content
|
|
27
|
+
- monitor
|
|
28
|
+
push:
|
|
29
|
+
branches:
|
|
30
|
+
- staging
|
|
31
|
+
- main
|
|
32
|
+
release:
|
|
33
|
+
types: [published]
|
|
34
|
+
|
|
35
|
+
concurrency:
|
|
36
|
+
group: treeseed-managed-${{ github.workflow }}-${{ github.ref }}
|
|
37
|
+
cancel-in-progress: false
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
classify:
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
outputs:
|
|
43
|
+
scope: ${{ steps.classify.outputs.scope }}
|
|
44
|
+
workflow_action: ${{ steps.classify.outputs.workflow_action }}
|
|
45
|
+
code_changed: ${{ steps.classify.outputs.code_changed }}
|
|
46
|
+
content_changed: ${{ steps.classify.outputs.content_changed }}
|
|
47
|
+
release_tag: ${{ steps.classify.outputs.release_tag }}
|
|
48
|
+
steps:
|
|
49
|
+
- name: Classify change
|
|
50
|
+
id: classify
|
|
51
|
+
shell: bash
|
|
52
|
+
run: |
|
|
53
|
+
set -euo pipefail
|
|
54
|
+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
55
|
+
scope="${{ inputs.environment }}"
|
|
56
|
+
workflow_action="${{ inputs.action_kind }}"
|
|
57
|
+
code_changed=$([[ "${workflow_action}" == "deploy_code" ]] && echo "true" || echo "false")
|
|
58
|
+
content_changed=$([[ "${workflow_action}" == "publish_content" ]] && echo "true" || echo "false")
|
|
59
|
+
release_tag="false"
|
|
60
|
+
elif [[ "${{ github.event_name }}" == "release" ]]; then
|
|
61
|
+
scope="prod"
|
|
62
|
+
workflow_action="deploy_code"
|
|
63
|
+
code_changed="true"
|
|
64
|
+
content_changed="false"
|
|
65
|
+
release_tag="true"
|
|
66
|
+
elif [[ "${{ github.ref_name }}" == "main" ]]; then
|
|
67
|
+
scope="prod"
|
|
68
|
+
workflow_action="deploy_code"
|
|
69
|
+
code_changed="true"
|
|
70
|
+
content_changed="false"
|
|
71
|
+
release_tag="false"
|
|
72
|
+
else
|
|
73
|
+
scope="staging"
|
|
74
|
+
workflow_action="deploy_code"
|
|
75
|
+
code_changed="true"
|
|
76
|
+
content_changed="false"
|
|
77
|
+
release_tag="false"
|
|
78
|
+
fi
|
|
79
|
+
echo "scope=${scope}" >> "${GITHUB_OUTPUT}"
|
|
80
|
+
echo "workflow_action=${workflow_action}" >> "${GITHUB_OUTPUT}"
|
|
81
|
+
echo "code_changed=${code_changed}" >> "${GITHUB_OUTPUT}"
|
|
82
|
+
echo "content_changed=${content_changed}" >> "${GITHUB_OUTPUT}"
|
|
83
|
+
echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}"
|
|
84
|
+
|
|
85
|
+
verify:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
needs: classify
|
|
88
|
+
permissions:
|
|
89
|
+
contents: read
|
|
90
|
+
__WORKING_DIRECTORY_BLOCK__
|
|
91
|
+
steps:
|
|
92
|
+
- name: Checkout
|
|
93
|
+
uses: actions/checkout@v4
|
|
94
|
+
|
|
95
|
+
- name: Setup Node
|
|
96
|
+
uses: actions/setup-node@v4
|
|
97
|
+
with:
|
|
98
|
+
node-version: 22
|
|
99
|
+
cache: npm
|
|
100
|
+
cache-dependency-path: __CACHE_DEPENDENCY_PATH__
|
|
101
|
+
|
|
102
|
+
- name: Install dependencies
|
|
103
|
+
shell: bash
|
|
104
|
+
run: npm ci --ignore-scripts
|
|
105
|
+
|
|
106
|
+
- name: Build workspace package artifacts
|
|
107
|
+
shell: bash
|
|
108
|
+
run: |
|
|
109
|
+
set -euo pipefail
|
|
110
|
+
for dir in packages/sdk packages/core packages/cli; do
|
|
111
|
+
if test -f "${dir}/package.json"; then
|
|
112
|
+
npm --prefix "${dir}" run build:dist
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
- name: Verify workspace
|
|
117
|
+
shell: bash
|
|
118
|
+
run: |
|
|
119
|
+
set -euo pipefail
|
|
120
|
+
npm run verify:local
|
|
121
|
+
|
|
122
|
+
request-managed-operation:
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
needs:
|
|
125
|
+
- classify
|
|
126
|
+
- verify
|
|
127
|
+
if: |
|
|
128
|
+
needs.classify.outputs.workflow_action == 'provision' ||
|
|
129
|
+
needs.classify.outputs.workflow_action == 'deploy_code' ||
|
|
130
|
+
needs.classify.outputs.workflow_action == 'publish_content' ||
|
|
131
|
+
needs.classify.outputs.workflow_action == 'monitor' ||
|
|
132
|
+
needs.classify.outputs.code_changed == 'true' ||
|
|
133
|
+
needs.classify.outputs.content_changed == 'true' ||
|
|
134
|
+
needs.classify.outputs.release_tag == 'true'
|
|
135
|
+
permissions:
|
|
136
|
+
contents: read
|
|
137
|
+
id-token: write
|
|
138
|
+
environment: ${{ needs.classify.outputs.scope == 'prod' && 'production' || 'staging' }}
|
|
139
|
+
env:
|
|
140
|
+
TREESEED_MARKET_API_BASE_URL: ${{ vars.TREESEED_MARKET_API_BASE_URL || vars.TREESEED_CENTRAL_MARKET_API_BASE_URL || 'https://api.treeseed.ai' }}
|
|
141
|
+
TREESEED_PROJECT_ID: ${{ inputs.project_id || vars.TREESEED_PROJECT_ID }}
|
|
142
|
+
TREESEED_WORKFLOW_ACTION: ${{ needs.classify.outputs.workflow_action }}
|
|
143
|
+
TREESEED_WORKFLOW_ENVIRONMENT: ${{ needs.classify.outputs.scope }}
|
|
144
|
+
steps:
|
|
145
|
+
- name: Request TreeSeed managed operation
|
|
146
|
+
shell: bash
|
|
147
|
+
run: |
|
|
148
|
+
set -euo pipefail
|
|
149
|
+
if [[ -z "${TREESEED_PROJECT_ID}" ]]; then
|
|
150
|
+
echo "TREESEED_PROJECT_ID is required for managed TreeSeed operations."
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
audience="treeseed:${TREESEED_PROJECT_ID}"
|
|
154
|
+
oidc_json="$(curl -fsSL -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${audience}")"
|
|
155
|
+
oidc_token="$(node -e "const fs=require('node:fs'); const data=JSON.parse(fs.readFileSync(0,'utf8')); process.stdout.write(data.value || '')" <<< "${oidc_json}")"
|
|
156
|
+
if [[ -z "${oidc_token}" ]]; then
|
|
157
|
+
echo "GitHub did not return an OIDC token."
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
request_json="$(OIDC_TOKEN="${oidc_token}" node --input-type=module <<'NODE'
|
|
161
|
+
const payload = {
|
|
162
|
+
oidcToken: process.env.OIDC_TOKEN,
|
|
163
|
+
actionKind: process.env.TREESEED_WORKFLOW_ACTION,
|
|
164
|
+
environment: process.env.TREESEED_WORKFLOW_ENVIRONMENT,
|
|
165
|
+
repository: process.env.GITHUB_REPOSITORY,
|
|
166
|
+
ref: process.env.GITHUB_REF,
|
|
167
|
+
refName: process.env.GITHUB_REF_NAME,
|
|
168
|
+
sha: process.env.GITHUB_SHA,
|
|
169
|
+
workflow: process.env.GITHUB_WORKFLOW,
|
|
170
|
+
workflowRef: process.env.GITHUB_WORKFLOW_REF,
|
|
171
|
+
runId: process.env.GITHUB_RUN_ID,
|
|
172
|
+
runAttempt: process.env.GITHUB_RUN_ATTEMPT,
|
|
173
|
+
};
|
|
174
|
+
process.stdout.write(JSON.stringify(payload));
|
|
175
|
+
NODE
|
|
176
|
+
)"
|
|
177
|
+
response="$(curl -fsSL -X POST "${TREESEED_MARKET_API_BASE_URL%/}/v1/projects/${TREESEED_PROJECT_ID}/ci/oidc/exchange" \
|
|
178
|
+
-H "content-type: application/json" \
|
|
179
|
+
--data "${request_json}")"
|
|
180
|
+
node -e "const r=JSON.parse(process.argv[1]); if (!r.ok) { throw new Error(r.error || 'TreeSeed operation request failed'); } console.log('Requested TreeSeed job ' + r.payload.job.id + ' (' + r.payload.job.status + ')');" "${response}"
|
|
181
|
+
echo "${response}" > treeseed-operation.json
|
|
182
|
+
|
|
183
|
+
- name: Wait for TreeSeed managed operation
|
|
184
|
+
shell: bash
|
|
185
|
+
run: |
|
|
186
|
+
set -euo pipefail
|
|
187
|
+
job_id="$(node -e "const r=require('./treeseed-operation.json'); process.stdout.write(r.payload.job.id)")"
|
|
188
|
+
operation_token="$(node -e "const r=require('./treeseed-operation.json'); process.stdout.write(r.payload.operationToken || '')")"
|
|
189
|
+
if [[ -z "${operation_token}" ]]; then
|
|
190
|
+
echo "TreeSeed did not return an operation status token."
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
for attempt in $(seq 1 180); do
|
|
194
|
+
status_json="$(curl -fsSL "${TREESEED_MARKET_API_BASE_URL%/}/v1/projects/${TREESEED_PROJECT_ID}/ci/jobs/${job_id}" \
|
|
195
|
+
-H "authorization: Bearer ${operation_token}")"
|
|
196
|
+
status="$(node -e "const r=JSON.parse(process.argv[1]); process.stdout.write(r.payload.job.status)" "${status_json}")"
|
|
197
|
+
echo "TreeSeed job ${job_id}: ${status}"
|
|
198
|
+
if [[ "${status}" == "succeeded" ]]; then
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
if [[ "${status}" == "failed" || "${status}" == "cancelled" ]]; then
|
|
202
|
+
node -e "const r=JSON.parse(process.argv[1]); console.error(JSON.stringify(r.payload.job.error || {}, null, 2));" "${status_json}"
|
|
203
|
+
exit 1
|
|
204
|
+
fi
|
|
205
|
+
sleep 10
|
|
206
|
+
done
|
|
207
|
+
echo "Timed out waiting for TreeSeed job ${job_id}."
|
|
208
|
+
exit 1
|