@treeseed/sdk 0.12.23 → 0.12.24

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.
@@ -170,6 +170,12 @@ export declare function listGitHubEnvironmentVariableNames(repository: string |
170
170
  }, environmentName: string, { client }?: {
171
171
  client?: GitHubApiClient;
172
172
  }): Promise<Set<string>>;
173
+ export declare function listGitHubEnvironmentVariables(repository: string | {
174
+ owner: string;
175
+ name: string;
176
+ }, environmentName: string, { client }?: {
177
+ client?: GitHubApiClient;
178
+ }): Promise<Map<string, string>>;
173
179
  export declare function upsertGitHubRepositorySecret(repository: string | {
174
180
  owner: string;
175
181
  name: string;
@@ -344,6 +344,19 @@ async function listGitHubEnvironmentVariableNames(repository, environmentName, {
344
344
  throw normalizeGitHubApiError(error, `Unable to list GitHub environment variables for ${owner}/${name}:${environmentName}`);
345
345
  }
346
346
  }
347
+ async function listGitHubEnvironmentVariables(repository, environmentName, { client = createGitHubApiClient() } = {}) {
348
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
349
+ try {
350
+ const paginate = client.paginate;
351
+ const variables = await withGitHubApiRetries(() => paginate(
352
+ "GET /repos/{owner}/{repo}/environments/{environment_name}/variables",
353
+ { owner, repo: name, environment_name: environmentName, per_page: 100 }
354
+ ));
355
+ return new Map(variables.map((entry) => [String(entry.name ?? "").trim(), String(entry.value ?? "")]).filter(([variableName]) => variableName.length > 0));
356
+ } catch (error) {
357
+ throw normalizeGitHubApiError(error, `Unable to list GitHub environment variables for ${owner}/${name}:${environmentName}`);
358
+ }
359
+ }
347
360
  async function encryptGitHubSecret(secret, key) {
348
361
  await sodium.ready;
349
362
  const messageBytes = Buffer.from(secret);
@@ -927,6 +940,7 @@ export {
927
940
  getLatestGitHubWorkflowRun,
928
941
  listGitHubEnvironmentSecretNames,
929
942
  listGitHubEnvironmentVariableNames,
943
+ listGitHubEnvironmentVariables,
930
944
  listGitHubRepositorySecretNames,
931
945
  listGitHubRepositoryVariableNames,
932
946
  maybeGetGitHubRepository,
@@ -446,11 +446,12 @@ function buildGitHubBindingAdapter(unitType) {
446
446
  const observed = await observeGitHubEnvironment(repository, environment, buildGitHubEnv(input));
447
447
  const names = unitType === "github-secret-binding" ? observed.secretNames : observed.variableNames;
448
448
  const exists = observed.exists && names.includes(name);
449
+ const value = unitType === "github-variable-binding" ? String(observed.variableValues?.[name] ?? "") : null;
449
450
  const warnings = observed.authAvailable === false ? [String(observed.error ?? "GitHub authentication is unavailable")] : observed.exists ? [] : [`GitHub environment ${environment} is missing`];
450
451
  return {
451
452
  ...genericObservedState(input, exists, warnings),
452
453
  status: exists ? "ready" : "pending",
453
- live: { repository, environment, name, exists, observed }
454
+ live: { repository, environment, name, exists, value, observed }
454
455
  };
455
456
  },
456
457
  diff(input) {
@@ -458,13 +459,24 @@ function buildGitHubBindingAdapter(unitType) {
458
459
  if (input.observed.live?.observed?.authAvailable === false) {
459
460
  return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
460
461
  }
461
- if (input.observed.exists) {
462
- return noopDiff();
463
- }
464
462
  const value = buildGitHubEnv(input)[name];
465
463
  if (!value) {
466
464
  return { action: "blocked", reasons: [`Missing local value for ${name}`], before: input.observed.live, after: input.unit.spec };
467
465
  }
466
+ if (input.observed.exists) {
467
+ if (unitType === "github-variable-binding") {
468
+ const observedValue = String(input.observed.live.value ?? "");
469
+ if (observedValue !== value) {
470
+ return {
471
+ action: "update",
472
+ reasons: [`GitHub variable ${name} value drifted`],
473
+ before: input.observed.live,
474
+ after: input.unit.spec
475
+ };
476
+ }
477
+ }
478
+ return noopDiff();
479
+ }
468
480
  return { action: "update", reasons: [`GitHub ${unitType === "github-secret-binding" ? "secret" : "variable"} ${name} is missing`], before: input.observed.live, after: input.unit.spec };
469
481
  },
470
482
  async apply(input) {
@@ -11,6 +11,9 @@ export declare function observeGitHubEnvironment(repository: string, environment
11
11
  environment: string;
12
12
  secretNames: string[];
13
13
  variableNames: string[];
14
+ variableValues: {
15
+ [k: string]: string;
16
+ };
14
17
  authAvailable?: undefined;
15
18
  error?: undefined;
16
19
  } | {
@@ -20,6 +23,7 @@ export declare function observeGitHubEnvironment(repository: string, environment
20
23
  environment: string;
21
24
  secretNames: never[];
22
25
  variableNames: never[];
26
+ variableValues: {};
23
27
  error: string;
24
28
  }>;
25
29
  export declare function ensureReconcileGitHubEnvironment(repository: string, environment: string, branchName: string | null, env: NodeJS.ProcessEnv | Record<string, string | undefined>): Promise<{
@@ -6,6 +6,7 @@ import {
6
6
  getLatestGitHubWorkflowRun,
7
7
  listGitHubEnvironmentSecretNames,
8
8
  listGitHubEnvironmentVariableNames,
9
+ listGitHubEnvironmentVariables,
9
10
  upsertGitHubEnvironmentSecret,
10
11
  upsertGitHubEnvironmentVariable,
11
12
  waitForGitHubWorkflowRunCompletion
@@ -19,16 +20,18 @@ function isGitHubAuthError(message) {
19
20
  async function observeGitHubEnvironment(repository, environment, env) {
20
21
  const client = createReconcileGitHubClient(env);
21
22
  try {
22
- const [secretNames, variableNames] = await Promise.all([
23
+ const [secretNames, variableNames, variableValues] = await Promise.all([
23
24
  listGitHubEnvironmentSecretNames(repository, environment, { client }),
24
- listGitHubEnvironmentVariableNames(repository, environment, { client })
25
+ listGitHubEnvironmentVariableNames(repository, environment, { client }),
26
+ listGitHubEnvironmentVariables(repository, environment, { client })
25
27
  ]);
26
28
  return {
27
29
  exists: true,
28
30
  repository,
29
31
  environment,
30
32
  secretNames: [...secretNames].sort(),
31
- variableNames: [...variableNames].sort()
33
+ variableNames: [...variableNames].sort(),
34
+ variableValues: Object.fromEntries([...variableValues.entries()].sort(([left], [right]) => left.localeCompare(right)))
32
35
  };
33
36
  } catch (error) {
34
37
  const message = error instanceof Error ? error.message : String(error);
@@ -40,6 +43,7 @@ async function observeGitHubEnvironment(repository, environment, env) {
40
43
  environment,
41
44
  secretNames: [],
42
45
  variableNames: [],
46
+ variableValues: {},
43
47
  error: message
44
48
  };
45
49
  }
@@ -51,6 +55,7 @@ async function observeGitHubEnvironment(repository, environment, env) {
51
55
  environment,
52
56
  secretNames: [],
53
57
  variableNames: [],
58
+ variableValues: {},
54
59
  error: message
55
60
  };
56
61
  }
@@ -1,4 +1,4 @@
1
- import { planTreeseedReconciliation, reconcileTreeseedTarget, type TreeseedReconcileTarget } from '../reconcile/index.js';
1
+ import { planTreeseedReconciliation, reconcileTreeseedTarget, type TreeseedDesiredUnit, type TreeseedReconcileTarget } from '../reconcile/index.js';
2
2
  import { STAGING_BRANCH } from '../operations/services/git-workflow.js';
3
3
  import type { TreeseedProofDriver } from '../operations/services/release-proof.js';
4
4
  import { type TreeseedWorkflowTiming } from '../operations/services/workflow-timing.js';
@@ -927,7 +927,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
927
927
  }[];
928
928
  reconcile: {
929
929
  target: TreeseedReconcileTarget;
930
- units: import("../reconcile/contracts.js").TreeseedDesiredUnit[];
930
+ units: TreeseedDesiredUnit[];
931
931
  plans: import("../reconcile/contracts.js").TreeseedReconcilePlan[];
932
932
  results: TreeseedReconcileResult[];
933
933
  state: import("../reconcile/contracts.js").TreeseedReconcileStateRecord;
@@ -1149,7 +1149,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1149
1149
  }[];
1150
1150
  reconcile: {
1151
1151
  target: TreeseedReconcileTarget;
1152
- units: import("../reconcile/contracts.js").TreeseedDesiredUnit[];
1152
+ units: TreeseedDesiredUnit[];
1153
1153
  plans: import("../reconcile/contracts.js").TreeseedReconcilePlan[];
1154
1154
  results: TreeseedReconcileResult[];
1155
1155
  state: import("../reconcile/contracts.js").TreeseedReconcileStateRecord;
@@ -5387,8 +5387,9 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
5387
5387
  };
5388
5388
  const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
5389
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"));
5390
- const rawUnitIds = new Set(rawUnits.map((unit) => unit.unitId));
5391
- const units = rawUnits.map((unit) => ({
5390
+ const unitsWithReleaseImageRefs = appendReleaseImageRefGitHubVariableBindings(rawUnits, input.releaseImageRefs ?? {});
5391
+ const rawUnitIds = new Set(unitsWithReleaseImageRefs.map((unit) => unit.unitId));
5392
+ const units = unitsWithReleaseImageRefs.map((unit) => ({
5392
5393
  ...unit,
5393
5394
  dependencies: unit.dependencies.filter((dependency) => rawUnitIds.has(dependency))
5394
5395
  }));
@@ -5461,6 +5462,38 @@ ${blockers.join("\n")}`, {
5461
5462
  ])
5462
5463
  });
5463
5464
  }
5465
+ function appendReleaseImageRefGitHubVariableBindings(units, releaseImageRefs) {
5466
+ const entries = Object.entries(releaseImageRefs).map(([name, value]) => [name.trim(), value.trim()]).filter(([name, value]) => name.length > 0 && value.length > 0);
5467
+ if (entries.length === 0) return units;
5468
+ const apiProductionEnvironment = units.find((unit) => unit.provider === "github" && unit.unitType === "github-environment" && unit.unitId === "github-environment:@treeseed/api:production");
5469
+ if (!apiProductionEnvironment) return units;
5470
+ const existingUnitIds = new Set(units.map((unit) => unit.unitId));
5471
+ const additions = entries.map(([variableName]) => {
5472
+ const unitId = `github-variable-binding:@treeseed/api:production:${variableName}`;
5473
+ if (existingUnitIds.has(unitId)) return null;
5474
+ return {
5475
+ ...apiProductionEnvironment,
5476
+ unitId,
5477
+ unitType: "github-variable-binding",
5478
+ logicalName: `@treeseed/api production ${variableName}`,
5479
+ dependencies: [apiProductionEnvironment.unitId],
5480
+ spec: {
5481
+ packageId: "@treeseed/api",
5482
+ packageRoot: apiProductionEnvironment.spec.packageRoot,
5483
+ repository: apiProductionEnvironment.spec.repository,
5484
+ environment: "production",
5485
+ variableName,
5486
+ envName: variableName
5487
+ },
5488
+ secrets: {},
5489
+ metadata: {
5490
+ ...apiProductionEnvironment.metadata,
5491
+ releaseImageRef: true
5492
+ }
5493
+ };
5494
+ }).filter((unit) => Boolean(unit));
5495
+ return additions.length > 0 ? [...units, ...additions] : units;
5496
+ }
5464
5497
  async function workflowRelease(helpers, input) {
5465
5498
  try {
5466
5499
  return await withContextEnv(helpers.context.env, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.23",
3
+ "version": "0.12.24",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {