@treeseed/sdk 0.12.21 → 0.12.22

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.
@@ -54,6 +54,7 @@ export declare class D1AuthProvider implements ApiAuthProvider {
54
54
  }>;
55
55
  createUser(input: {
56
56
  email?: string | null;
57
+ username?: string | null;
57
58
  displayName?: string | null;
58
59
  metadata?: Record<string, unknown>;
59
60
  }): Promise<{
@@ -31,6 +31,8 @@ export declare class D1AuthStore {
31
31
  private loadUser;
32
32
  private loadIdentityByProvider;
33
33
  private loadUserByVerifiedEmail;
34
+ private loadUserByUsername;
35
+ private canAdoptUsernameMatch;
34
36
  private rolesForUser;
35
37
  private permissionsForUser;
36
38
  private permissionsForRoles;
@@ -250,6 +250,20 @@ class D1AuthStore {
250
250
  [email]
251
251
  );
252
252
  }
253
+ async loadUserByUsername(username) {
254
+ return this.first(
255
+ `SELECT * FROM users WHERE LOWER(username) = LOWER(?) AND status = 'active' LIMIT 1`,
256
+ [username]
257
+ );
258
+ }
259
+ canAdoptUsernameMatch(identity, user) {
260
+ if (!user?.id || !identity.username) return false;
261
+ const profile = identity.profile && typeof identity.profile === "object" ? identity.profile : {};
262
+ if (identity.provider === "acceptance" || profile.acceptance === true) return true;
263
+ const existingEmail = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
264
+ const requestedEmail = typeof identity.email === "string" ? identity.email.trim().toLowerCase() : "";
265
+ return Boolean(requestedEmail && existingEmail && requestedEmail === existingEmail && identity.emailVerified);
266
+ }
253
267
  async rolesForUser(userId) {
254
268
  const rows = await this.all(
255
269
  `SELECT roles.key AS key
@@ -381,7 +395,9 @@ class D1AuthStore {
381
395
  const existingIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
382
396
  let userId = existingIdentity?.user_id;
383
397
  if (!userId) {
384
- const linkedUser = identity.email && identity.emailVerified ? await this.loadUserByVerifiedEmail(identity.email) : null;
398
+ const emailLinkedUser = identity.email && identity.emailVerified ? await this.loadUserByVerifiedEmail(identity.email) : null;
399
+ const usernameLinkedUser = !emailLinkedUser && identity.username ? await this.loadUserByUsername(identity.username) : null;
400
+ const linkedUser = emailLinkedUser ?? (this.canAdoptUsernameMatch(identity, usernameLinkedUser) ? usernameLinkedUser : null);
385
401
  userId = linkedUser?.id ?? randomUUID();
386
402
  if (linkedUser) {
387
403
  await this.run(
@@ -778,7 +778,27 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
778
778
  });
779
779
  monitorErrorStartedAt = null;
780
780
  lastMonitorError = null;
781
- const match = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).find((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
781
+ const matchingRuns = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).filter((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
782
+ const failedMatch = matchingRuns.find((run) => run.status === "completed" && run.conclusion !== "success");
783
+ if (failedMatch?.id) {
784
+ const failedJobs = await listWorkflowJobsForProgress(client, owner, name, failedMatch.id);
785
+ emitProgress("completed", failedMatch, failedJobs);
786
+ return {
787
+ status: "completed",
788
+ repository: `${owner}/${name}`,
789
+ workflow,
790
+ runId: failedMatch.id,
791
+ headSha: failedMatch.headSha,
792
+ branch: failedMatch.headBranch,
793
+ createdAt: failedMatch.createdAt,
794
+ updatedAt: failedMatch.updatedAt,
795
+ conclusion: failedMatch.conclusion,
796
+ url: failedMatch.url,
797
+ jobs: failedJobs,
798
+ failedJobs: failedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
799
+ };
800
+ }
801
+ const match = matchingRuns[0];
782
802
  if (!match?.id) {
783
803
  emitProgress("waiting");
784
804
  if (dispatchIfMissing && branch && !dispatchedMissingRun && Date.now() - startedAt >= dispatchAfterSeconds * 1e3) {
@@ -1,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import { loadTreeseedPlatformConfig } from "../../platform/config.js";
2
3
  import { resolveTreeseedLaunchEnvironment } from "./config-runtime.js";
3
4
  import {
@@ -21,7 +22,7 @@ const DEFAULT_RETRY_INTERVAL_MS = 1500;
21
22
  const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 60;
22
23
  const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_INTERVAL_MS = 5e3;
23
24
  function sleep(ms) {
24
- return new Promise((resolve) => setTimeout(resolve, ms));
25
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
25
26
  }
26
27
  function liveCheckErrorMessage(error, fallback) {
27
28
  const message = error instanceof Error ? error.message : String(error ?? "");
@@ -125,12 +126,22 @@ function selectedServiceKeySet(options) {
125
126
  function serviceIsSelected(selected, serviceKey) {
126
127
  return selected.size === 0 || selected.has(serviceKey);
127
128
  }
129
+ function serviceMatchesAppSelection(service, tenantRoot, appId, applications) {
130
+ if (!appId) return true;
131
+ if (service.application?.id === appId) return true;
132
+ if (!service.application) {
133
+ const rootApplication = applications.find((application) => application.root === tenantRoot);
134
+ return rootApplication?.id === appId || rootApplication?.relativeRoot === appId;
135
+ }
136
+ return false;
137
+ }
128
138
  function treeseedDatabaseDescriptors(tenantRoot, options) {
129
139
  const descriptors = [];
130
140
  const rootConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: options.target, env: process.env }).deployConfig;
141
+ const applications = discoverTreeseedApplications(tenantRoot);
131
142
  const candidates = [
132
- { applicationId: "web", applicationRoot: tenantRoot, config: rootConfig },
133
- ...discoverTreeseedApplications(tenantRoot).map((application) => ({
143
+ { applicationId: null, applicationRoot: tenantRoot, config: rootConfig },
144
+ ...applications.map((application) => ({
134
145
  applicationId: application.id,
135
146
  applicationRoot: application.root,
136
147
  config: application.config
@@ -152,8 +163,9 @@ function treeseedDatabaseDescriptors(tenantRoot, options) {
152
163
  return descriptors;
153
164
  }
154
165
  async function verifyRailwayPostgresTopology(input) {
166
+ const descriptorRoot = resolve(input.descriptor.applicationRoot);
155
167
  const ownerService = input.configuredServices.find(
156
- (service) => ["api", "operationsRunner"].includes(service.key) && (!input.descriptor.applicationId || service.application?.id === input.descriptor.applicationId)
168
+ (service) => ["api", "operationsRunner"].includes(service.key) && (!input.descriptor.applicationId || service.application?.id === input.descriptor.applicationId || service.application?.root === descriptorRoot || !service.application && resolve(service.rootDir) === descriptorRoot)
157
169
  );
158
170
  if (!ownerService) {
159
171
  input.issues.push(`${input.descriptor.serviceName}: no Railway API or operations runner service is configured to own the database.`);
@@ -215,10 +227,11 @@ async function collectRailwayObservations(options) {
215
227
  const inspectedVolumeScopes = /* @__PURE__ */ new Set();
216
228
  const inspectedRunnerScopes = /* @__PURE__ */ new Set();
217
229
  const selectedServiceKeys = selectedServiceKeySet(options);
230
+ const applications = discoverTreeseedApplications(options.tenantRoot);
218
231
  try {
219
232
  const workspace = await resolveRailwayWorkspaceContext({ env: options.env, fetchImpl: options.fetchImpl });
220
233
  const projects = await listRailwayProjects({ workspaceId: workspace.id, env: options.env, fetchImpl: options.fetchImpl });
221
- const configuredServices = configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key));
234
+ const configuredServices = configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => serviceMatchesAppSelection(entry, options.tenantRoot, options.appId, applications)).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key));
222
235
  if (selectedServiceKeys.size === 0 || selectedServiceKeys.has("api") || selectedServiceKeys.has("operationsRunner")) {
223
236
  for (const descriptor of treeseedDatabaseDescriptors(options.tenantRoot, options)) {
224
237
  await verifyRailwayPostgresTopology({ descriptor, configuredServices, projects, options, issues });
@@ -422,7 +435,8 @@ async function collectHttpObservations(options) {
422
435
  const urls = /* @__PURE__ */ new Set();
423
436
  const fallbacks = /* @__PURE__ */ new Map();
424
437
  const selectedServiceKeys = selectedServiceKeySet(options);
425
- const selectedApplication = options.appId ? discoverTreeseedApplications(options.tenantRoot).find((application) => application.id === options.appId || application.relativeRoot === options.appId) : null;
438
+ const applications = discoverTreeseedApplications(options.tenantRoot);
439
+ const selectedApplication = options.appId ? applications.find((application) => application.id === options.appId || application.relativeRoot === options.appId) : null;
426
440
  const webHttpSelected = selectedServiceKeys.size === 0 || selectedServiceKeys.has("web");
427
441
  if (webHttpSelected && (!options.appId || options.appId === "web" || selectedApplication?.roles.includes("web"))) {
428
442
  const webConfig = selectedWebConfig(deployConfig, selectedApplication);
@@ -441,7 +455,7 @@ async function collectHttpObservations(options) {
441
455
  }
442
456
  }
443
457
  }
444
- for (const service of configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key))) {
458
+ for (const service of configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => serviceMatchesAppSelection(entry, options.tenantRoot, options.appId, applications)).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key))) {
445
459
  const serviceConfig = deployConfig.services?.[service.key];
446
460
  const domain = service.publicBaseUrl ?? serviceConfig?.environments?.[options.target]?.baseUrl ?? serviceConfig?.environments?.[options.target]?.domain ?? (service.key === "api" ? deployConfig.surfaces?.api?.environments?.[options.target]?.domain : null);
447
461
  const baseUrl = urlForDomain(domain);
@@ -3877,7 +3877,8 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3877
3877
  services: rendered.serviceNames,
3878
3878
  volumes: rendered.volumeNames,
3879
3879
  database: rendered.databaseName,
3880
- scope
3880
+ scope,
3881
+ serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null]))
3881
3882
  });
3882
3883
  if (!validation.ok && effectiveIacInput.database && !effectiveIacInput.database.useNativePostgres && railwayIacPlanDeletesResource(plan.changeSet, effectiveIacInput.database.serviceName)) {
3883
3884
  traceRailwayReconcile(topology.env, "sync:iac-native-postgres-adopt", `${project.name}/${environment.name}:${effectiveIacInput.database.serviceName}`);
@@ -3899,7 +3900,8 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3899
3900
  services: rendered.serviceNames,
3900
3901
  volumes: rendered.volumeNames,
3901
3902
  database: rendered.databaseName,
3902
- scope
3903
+ scope,
3904
+ serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null]))
3903
3905
  });
3904
3906
  }
3905
3907
  if (!validation.ok) {
@@ -2,6 +2,7 @@ import {
2
2
  createGitHubApiClient,
3
3
  dispatchGitHubWorkflowRun,
4
4
  ensureGitHubActionsEnvironment,
5
+ formatGitHubWorkflowFailure,
5
6
  getLatestGitHubWorkflowRun,
6
7
  listGitHubEnvironmentSecretNames,
7
8
  listGitHubEnvironmentVariableNames,
@@ -106,6 +107,26 @@ async function dispatchReconcileGitHubWorkflow(input) {
106
107
  branch: input.branch,
107
108
  timeoutSeconds: input.timeoutMs ? Math.ceil(input.timeoutMs / 1e3) : void 0
108
109
  }) : null;
110
+ if (completed && completed.conclusion !== "success") {
111
+ const failedJob = completed.failedJobs?.[0] ?? completed.jobs?.find((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped") ?? null;
112
+ const failedStep = failedJob?.steps?.find((step) => step.conclusion && step.conclusion !== "success" && step.conclusion !== "skipped") ?? null;
113
+ const failure = formatGitHubWorkflowFailure({
114
+ repository: input.repository,
115
+ workflow: input.workflow,
116
+ runId: completed.runId,
117
+ runUrl: completed.url,
118
+ conclusion: completed.conclusion,
119
+ failedJobName: failedJob?.name,
120
+ lastActiveStep: failedStep?.name,
121
+ message: `GitHub workflow ${input.workflow} in ${input.repository} completed with conclusion ${completed.conclusion ?? "unknown"}.`,
122
+ resumeSafe: false
123
+ });
124
+ throw new Error([
125
+ failure.summary,
126
+ failure.runUrl ? `Run: ${failure.runUrl}` : null,
127
+ failure.inspectCommand ? `Inspect: ${failure.inspectCommand}` : null
128
+ ].filter(Boolean).join("\n"));
129
+ }
109
130
  return { dispatch, latest, completed };
110
131
  }
111
132
  export {
@@ -60,6 +60,7 @@ export declare function validateRailwayIacChangeSet(changeSet: RailwayChangeSet
60
60
  volumes: string[];
61
61
  database: string | null;
62
62
  scope: string;
63
+ serviceSourceModes?: Record<string, string | null | undefined>;
63
64
  }): RailwayIacValidationResult;
64
65
  export declare function planRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacPlanResponse>;
65
66
  export declare function applyRailwayIacProject(input: TreeseedRailwayIacProjectInput, rendered?: TreeseedRailwayIacRenderResult): Promise<RailwayIacApplyResponse>;
@@ -203,6 +203,28 @@ ${declarations.join("\n")}
203
203
  function changeName(change) {
204
204
  return String(change?.resource?.name ?? change?.previous?.name ?? change?.address ?? change?.path ?? "");
205
205
  }
206
+ function changeFieldText(change) {
207
+ return [
208
+ change?.field,
209
+ change?.path,
210
+ change?.address,
211
+ change?.summary
212
+ ].map((value) => String(value ?? "").toLowerCase()).join(" ");
213
+ }
214
+ function isRailwaySourceChange(change) {
215
+ const field = String(change?.field ?? "").toLowerCase();
216
+ const path = String(change?.path ?? "").toLowerCase();
217
+ const summary = String(change?.summary ?? "").toLowerCase();
218
+ return field === "source" || /\.source\b/u.test(path) || /source/u.test(summary) && !/\b(env|environment|variable|variables)\b/u.test(summary);
219
+ }
220
+ function isRailwayImageSourceChange(change) {
221
+ if (!isRailwaySourceChange(change)) return false;
222
+ return /image|docker-image/u.test(changeFieldText(change));
223
+ }
224
+ function isRailwayGitSourceChange(change) {
225
+ if (!isRailwaySourceChange(change)) return false;
226
+ return /github|repo|branch/u.test(changeFieldText(change));
227
+ }
206
228
  function validateRailwayIacChangeSet(changeSet, desiredNames) {
207
229
  const blockedReasons = [];
208
230
  const destructiveChanges = [];
@@ -210,6 +232,7 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
210
232
  const created = new Set((changeSet?.changes ?? []).filter((change) => change.kind === "resource.create").map((change) => changeName(change)));
211
233
  for (const change of changeSet?.changes ?? []) {
212
234
  const name = changeName(change);
235
+ const sourceMode = desiredNames.serviceSourceModes?.[name] ?? desiredNames.serviceSourceModes?.[name.replace(/^(service|database)\./u, "")] ?? null;
213
236
  if (change.kind === "resource.delete") {
214
237
  destructiveChanges.push(change.summary);
215
238
  blockedReasons.push(`Railway IaC plan would delete resource ${name || change.summary}; hosting reconciliation only updates or creates resources. Use the explicit destroy workflow for deletions.`);
@@ -217,10 +240,10 @@ function validateRailwayIacChangeSet(changeSet, desiredNames) {
217
240
  blockedReasons.push(`Railway IaC plan would delete desired resource ${name}.`);
218
241
  }
219
242
  }
220
- if (desiredNames.scope === "staging" && change.kind === "resource.update" && /image/iu.test(String(change.summary))) {
243
+ if (desiredNames.scope === "staging" && change.kind === "resource.update" && isRailwayImageSourceChange(change) && (!sourceMode || sourceMode === "image")) {
221
244
  blockedReasons.push(`Railway IaC plan would switch staging resource ${name} to an image source.`);
222
245
  }
223
- if (desiredNames.scope === "prod" && change.kind === "resource.update" && /github|repo|branch/iu.test(String(change.summary))) {
246
+ if (desiredNames.scope === "prod" && change.kind === "resource.update" && isRailwayGitSourceChange(change) && (!sourceMode || sourceMode === "git")) {
224
247
  blockedReasons.push(`Railway IaC plan would switch production resource ${name} to a Git source.`);
225
248
  }
226
249
  }
@@ -1010,7 +1010,10 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1010
1010
  }[];
1011
1011
  blockers: string[];
1012
1012
  };
1013
- releaseGates: Record<string, unknown>;
1013
+ releaseGates: {
1014
+ workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
1015
+ gates: Record<string, unknown>;
1016
+ };
1014
1017
  workspaceUnlink: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
1015
1018
  releaseMetadata: {
1016
1019
  versions: {
@@ -1100,6 +1103,24 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1100
1103
  publishedArtifacts: {
1101
1104
  checks: PublishedArtifactCheck[];
1102
1105
  };
1106
+ productionPackageDeployWorkflows: {
1107
+ workflowGates: Record<string, unknown>[] | {
1108
+ name: string;
1109
+ repository: string | null;
1110
+ workflow: string;
1111
+ branch: string;
1112
+ headSha: string;
1113
+ status: string;
1114
+ reason: string;
1115
+ conclusion: null;
1116
+ runId: null;
1117
+ url: null;
1118
+ createdAt: null;
1119
+ updatedAt: null;
1120
+ timeoutSeconds: number | null;
1121
+ cached: boolean;
1122
+ }[];
1123
+ };
1103
1124
  productionHosting: {
1104
1125
  status: "skipped";
1105
1126
  reason: string;
@@ -132,6 +132,10 @@ import {
132
132
  import { classifyTreeseedGitMode, runTreeseedGit, runTreeseedGitOk, runTreeseedGitText } from "../operations/services/git-runner.js";
133
133
  import { collectTreeseedDeploymentReadiness } from "../operations/services/deployment-readiness.js";
134
134
  import { collectTreeseedLiveHostedServiceChecks } from "../operations/services/live-hosted-service-checks.js";
135
+ import {
136
+ configuredRailwayServices,
137
+ waitForRailwayManagedDeploymentsSettled
138
+ } from "../operations/services/railway-deploy.js";
135
139
  import { discoverTreeseedApplications } from "../hosting/apps.js";
136
140
  import { compileTreeseedHostingGraph } from "../hosting/graph.js";
137
141
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
@@ -228,7 +232,7 @@ function readPackageScript(root, packageDir, scriptName) {
228
232
  }
229
233
  function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
230
234
  const packages = [
231
- { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
235
+ { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/index.js", "dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
232
236
  { name: "@treeseed/ui", dir: "packages/ui", artifacts: ["dist/index.js"] },
233
237
  { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/manager.js", "dist/provider/runner.js"] },
234
238
  { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
@@ -582,6 +586,23 @@ ${status.blockers.join("\n")}`, {
582
586
  details: { environment, selector, status, reconcile }
583
587
  });
584
588
  }
589
+ const selectedRailwayServiceNames = new Set(graph.units.filter((unit) => unit.host.id === "railway").map((unit) => typeof unit.config.serviceName === "string" ? unit.config.serviceName : null).filter((value) => Boolean(value)));
590
+ const selectedRailwayServices = configuredRailwayServices(root, environment, env).filter((service) => selectedRailwayServiceNames.has(service.serviceName));
591
+ if (selectedRailwayServices.length > 0) {
592
+ const deployments = await waitForRailwayManagedDeploymentsSettled(root, environment, {
593
+ services: selectedRailwayServices,
594
+ env,
595
+ timeoutMs: operation === "release" ? 9e5 : 6e5,
596
+ onProgress: (line, stream) => helpers.write(`[${operation}][railway] ${line}`, stream)
597
+ });
598
+ if (!deployments.ok) {
599
+ const deploymentFailures = deployments.checks.filter((check) => check.ok !== true && check.skipped !== true).map((check) => `${check.serviceName ?? check.service}: ${check.message ?? check.status ?? "deployment did not settle"}`);
600
+ workflowError(operation, "hosted_deployment_failed", `Hosted Railway deployments for ${environment} did not settle:
601
+ ${deploymentFailures.join("\n")}`, {
602
+ details: { environment, selector, deployments, reconcile }
603
+ });
604
+ }
605
+ }
585
606
  const live = await collectTreeseedLiveHostedServiceChecks({
586
607
  tenantRoot: root,
587
608
  target: environment,
@@ -1979,6 +2000,48 @@ function releaseWorkflowForPackage(root, packageName) {
1979
2000
  void packageName;
1980
2001
  return "publish.yml";
1981
2002
  }
2003
+ function productionDeployWorkflowForPackage(root, packageName) {
2004
+ const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === packageName || entry.name === packageName);
2005
+ const repoPath = adapter?.dir;
2006
+ if (!repoPath || !existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
2007
+ return null;
2008
+ }
2009
+ if (!workflowFileExists(repoPath, "deploy.yml")) {
2010
+ return null;
2011
+ }
2012
+ return "deploy.yml";
2013
+ }
2014
+ function tagCommitSha(repoDir, tagName) {
2015
+ try {
2016
+ return runGit(["rev-list", "-n", "1", tagName], { cwd: repoDir, capture: true }).trim();
2017
+ } catch {
2018
+ return "";
2019
+ }
2020
+ }
2021
+ function productionPackageDeployGates(root, versions) {
2022
+ return discoverTreeseedPackageAdapters(root).flatMap((adapter) => {
2023
+ const name = adapter.id;
2024
+ const version = versions.get(name);
2025
+ const path = adapter.dir;
2026
+ const workflow = productionDeployWorkflowForPackage(root, name);
2027
+ if (!name || !version || !path || !workflow) {
2028
+ return [];
2029
+ }
2030
+ const headSha = tagCommitSha(path, version);
2031
+ if (!headSha) {
2032
+ workflowError("release", "github_workflow_failed", `${name} ${workflow} cannot be checked because release tag ${version} is missing locally.`, {
2033
+ details: { packageName: name, workflow, version, repoPath: path }
2034
+ });
2035
+ }
2036
+ return [hostedDeployGate({
2037
+ name,
2038
+ repoPath: path,
2039
+ workflow,
2040
+ branch: version,
2041
+ headSha
2042
+ })];
2043
+ });
2044
+ }
1982
2045
  function prepareAdapterReleaseMetadata(root, pkg, version) {
1983
2046
  const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
1984
2047
  if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
@@ -5323,7 +5386,7 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
5323
5386
  provider: ["treeseed"]
5324
5387
  };
5325
5388
  const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
5326
- const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")) && (includeHostedReleaseGates || unit.unitType !== "release-gate:hosted-reconcile" && unit.unitType !== "release-gate:live-verify") || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
5389
+ const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")) && unit.unitType !== "release-gate:npm-publish" && unit.unitType !== "release-gate:image-publish" && (includeHostedReleaseGates || unit.unitType !== "release-gate:hosted-reconcile" && unit.unitType !== "release-gate:live-verify") || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
5327
5390
  const rawUnitIds = new Set(rawUnits.map((unit) => unit.unitId));
5328
5391
  const units = rawUnits.map((unit) => ({
5329
5392
  ...unit,
@@ -5528,6 +5591,7 @@ ${blockers.join("\n")}`, {
5528
5591
  };
5529
5592
  }),
5530
5593
  { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5594
+ { id: "production-package-deploy-workflows", description: "Wait for production package deploy workflows before live verification", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5531
5595
  { id: "production-hosting", description: "Reconcile and live-verify production hosted resources before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5532
5596
  { id: "production-api-guarantees", description: "Run production API release guarantees before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5533
5597
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
@@ -5551,21 +5615,25 @@ ${blockers.join("\n")}`, {
5551
5615
  ...releaseBasePayload,
5552
5616
  freshArchivedRuns: freshPreparation.archived
5553
5617
  }));
5554
- const releaseGates = await executeJournalStep(root, workflowRun.runId, "release-gates", async () => runReleaseGateReconcileFacade(
5555
- "release",
5556
- helpers,
5557
- root,
5558
- { kind: "persistent", scope: "prod" },
5559
- {
5560
- execute: true,
5561
- verifyDeployedResources: effectiveInput.verifyDeployedResources,
5562
- releaseImageRefs: productionReleaseImageRefEnv(selectedVersions)
5563
- },
5564
- {
5565
- ...releaseBasePayload,
5566
- freshArchivedRuns: freshPreparation.archived
5567
- }
5568
- ));
5618
+ const releaseGates = await executeJournalStep(root, workflowRun.runId, "release-gates", async () => {
5619
+ const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
5620
+ const gates = await runReleaseGateReconcileFacade(
5621
+ "release",
5622
+ helpers,
5623
+ root,
5624
+ { kind: "persistent", scope: "prod" },
5625
+ {
5626
+ execute: true,
5627
+ verifyDeployedResources: effectiveInput.verifyDeployedResources,
5628
+ releaseImageRefs: productionReleaseImageRefEnv(selectedVersions)
5629
+ },
5630
+ {
5631
+ ...releaseBasePayload,
5632
+ freshArchivedRuns: freshPreparation.archived
5633
+ }
5634
+ );
5635
+ return { workspaceLinks: workspaceLinks2, gates };
5636
+ });
5569
5637
  const workspaceUnlink = await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
5570
5638
  const releaseMetadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
5571
5639
  applyStableWorkspaceVersionChanges(root, allVersions);
@@ -5639,6 +5707,17 @@ ${rendered}`);
5639
5707
  packageReleases.push(packageRelease);
5640
5708
  }
5641
5709
  const publishedArtifacts = await executeJournalStep(root, workflowRun.runId, "verify-published-artifacts", () => verifyPublishedReleaseArtifacts(selectedVersions));
5710
+ const productionPackageDeployWorkflows = await executeJournalStep(root, workflowRun.runId, "production-package-deploy-workflows", () => {
5711
+ const deployGates = productionPackageDeployGates(root, allVersions);
5712
+ if (deployGates.length === 0) {
5713
+ return { workflowGates: [], status: "skipped", reason: "no selected production package deploy workflows" };
5714
+ }
5715
+ return waitForWorkflowGates("release", deployGates, ciMode, {
5716
+ root,
5717
+ runId: workflowRun.runId,
5718
+ onProgress: (line, stream) => helpers.write(line, stream)
5719
+ }).then((workflowGates) => ({ workflowGates }));
5720
+ });
5642
5721
  const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", productionReleaseImageRefEnv(selectedVersions), { liveAppId: "api" }));
5643
5722
  const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
5644
5723
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
@@ -5718,6 +5797,7 @@ ${rendered}`);
5718
5797
  rootRelease,
5719
5798
  publishWait: publishWait.workflowGates,
5720
5799
  publishedArtifacts,
5800
+ productionPackageDeployWorkflows,
5721
5801
  productionHosting,
5722
5802
  productionApiGuarantees,
5723
5803
  productionWebVerification,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.21",
3
+ "version": "0.12.22",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {