@treeseed/sdk 0.8.1 → 0.8.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.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 +67 -2
- 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
|
}
|
|
@@ -1284,12 +1304,29 @@ function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
|
|
|
1284
1304
|
}
|
|
1285
1305
|
return { archived, blockers };
|
|
1286
1306
|
}
|
|
1287
|
-
function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
|
|
1307
|
+
function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, options = {}) {
|
|
1288
1308
|
if (branch !== STAGING_BRANCH) return null;
|
|
1309
|
+
const currentHeads = Object.fromEntries([
|
|
1310
|
+
[rootRepo.name, rootRepo.commitSha ?? null],
|
|
1311
|
+
...packageReports.map((report) => [report.name, report.commitSha ?? null])
|
|
1312
|
+
]);
|
|
1289
1313
|
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
1290
1314
|
if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
|
|
1291
1315
|
return false;
|
|
1292
1316
|
}
|
|
1317
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1318
|
+
currentBranch: branch,
|
|
1319
|
+
currentHeads
|
|
1320
|
+
});
|
|
1321
|
+
if (classification.state !== "resumable") {
|
|
1322
|
+
if (options.archiveStale && classification.state === "stale") {
|
|
1323
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1324
|
+
...classification,
|
|
1325
|
+
reasons: ["release auto-resume skipped stale failed release", ...classification.reasons]
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1293
1330
|
const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
|
|
1294
1331
|
const nextStep = nextPendingJournalStep(journal);
|
|
1295
1332
|
if (releaseRunHasCompletedMutation(journal)) {
|
|
@@ -3025,6 +3062,13 @@ async function workflowSave(helpers, input) {
|
|
|
3025
3062
|
};
|
|
3026
3063
|
}
|
|
3027
3064
|
}
|
|
3065
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
|
|
3066
|
+
"save",
|
|
3067
|
+
root,
|
|
3068
|
+
helpers,
|
|
3069
|
+
scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
|
|
3070
|
+
{ enabled: effectiveInput.verifyDeployedResources === true, strict: true }
|
|
3071
|
+
);
|
|
3028
3072
|
const payload = {
|
|
3029
3073
|
mode: saveResult?.mode ?? mode,
|
|
3030
3074
|
branch,
|
|
@@ -3052,6 +3096,7 @@ async function workflowSave(helpers, input) {
|
|
|
3052
3096
|
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
3053
3097
|
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
3054
3098
|
releaseCandidate,
|
|
3099
|
+
hostingAudit,
|
|
3055
3100
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3056
3101
|
};
|
|
3057
3102
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
@@ -3549,6 +3594,13 @@ async function workflowStage(helpers, input) {
|
|
|
3549
3594
|
blockers: []
|
|
3550
3595
|
});
|
|
3551
3596
|
const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("stage", root, stageReleasePlan));
|
|
3597
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
|
|
3598
|
+
"stage",
|
|
3599
|
+
root,
|
|
3600
|
+
helpers,
|
|
3601
|
+
"staging",
|
|
3602
|
+
{ enabled: effectiveInput.verifyDeployedResources === true, strict: true }
|
|
3603
|
+
);
|
|
3552
3604
|
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
3605
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
3554
3606
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
@@ -3605,6 +3657,7 @@ async function workflowStage(helpers, input) {
|
|
|
3605
3657
|
workspaceLinks,
|
|
3606
3658
|
ciMode,
|
|
3607
3659
|
workflowGates: stagingWait.workflowGates,
|
|
3660
|
+
hostingAudit,
|
|
3608
3661
|
worktreeCleanup,
|
|
3609
3662
|
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3610
3663
|
managedWorktree,
|
|
@@ -3678,7 +3731,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3678
3731
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3679
3732
|
const freshRelease = input.fresh === true && !explicitResumeRunId;
|
|
3680
3733
|
const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
3681
|
-
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3734
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: true }) : null;
|
|
3682
3735
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3683
3736
|
const effectiveInput = autoResumeRun ? {
|
|
3684
3737
|
...autoResumeRun.input,
|
|
@@ -3861,6 +3914,11 @@ async function workflowRelease(helpers, input) {
|
|
|
3861
3914
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3862
3915
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3863
3916
|
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
3917
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
3918
|
+
const hostingAudit2 = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
|
|
3919
|
+
enabled: true,
|
|
3920
|
+
strict: false
|
|
3921
|
+
});
|
|
3864
3922
|
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
|
|
3865
3923
|
version: rootVersion,
|
|
3866
3924
|
changelog: rootRelease2?.changelog ?? null
|
|
@@ -3893,6 +3951,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3893
3951
|
workspaceLinks: workspaceLinks2,
|
|
3894
3952
|
ciMode,
|
|
3895
3953
|
workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
|
|
3954
|
+
hostingAudit: hostingAudit2,
|
|
3896
3955
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
3897
3956
|
};
|
|
3898
3957
|
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
@@ -4162,6 +4221,11 @@ async function workflowRelease(helpers, input) {
|
|
|
4162
4221
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
4163
4222
|
}).then((workflowGates) => ({ workflowGates })));
|
|
4164
4223
|
const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
|
|
4224
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
4225
|
+
const hostingAudit = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
|
|
4226
|
+
enabled: true,
|
|
4227
|
+
strict: false
|
|
4228
|
+
});
|
|
4165
4229
|
const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
|
|
4166
4230
|
version: rootVersion,
|
|
4167
4231
|
changelog: rootRelease?.changelog ?? null,
|
|
@@ -4214,6 +4278,7 @@ async function workflowRelease(helpers, input) {
|
|
|
4214
4278
|
...packageReports.flatMap((report) => report.workflowGates),
|
|
4215
4279
|
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
4216
4280
|
],
|
|
4281
|
+
hostingAudit,
|
|
4217
4282
|
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
4218
4283
|
};
|
|
4219
4284
|
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
|