@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.
@@ -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
+ };
@@ -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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -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