@treeseed/sdk 0.12.22 → 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.
- package/dist/operations/services/github-api.d.ts +6 -0
- package/dist/operations/services/github-api.js +14 -0
- package/dist/operations/services/project-platform.js +13 -6
- package/dist/reconcile/builtin-adapters.js +16 -4
- package/dist/reconcile/providers/github-private.d.ts +4 -0
- package/dist/reconcile/providers/github-private.js +8 -3
- package/dist/workflow/operations.d.ts +3 -3
- package/dist/workflow/operations.js +35 -2
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +1 -0
|
@@ -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,
|
|
@@ -192,8 +192,13 @@ function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
return result;
|
|
194
194
|
}
|
|
195
|
+
const WRANGLER_TRANSIENT_MAX_ATTEMPTS = 6;
|
|
196
|
+
const WRANGLER_COMMAND_TIMEOUT_MS = 18e4;
|
|
197
|
+
function wranglerTransientRetryDelayMs(attempt) {
|
|
198
|
+
return Math.min(5e3 * 2 ** (attempt - 1), 6e4);
|
|
199
|
+
}
|
|
195
200
|
function isTransientWranglerOutput(output) {
|
|
196
|
-
return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|connectivity issue|internal error|aborted/i.test(output);
|
|
201
|
+
return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|connectivity issue|internal error|code:\s*7500|aborted/i.test(output);
|
|
197
202
|
}
|
|
198
203
|
async function runPrefixedWranglerWithRetry(tenantRoot, args, {
|
|
199
204
|
env: env2 = {},
|
|
@@ -201,7 +206,7 @@ async function runPrefixedWranglerWithRetry(tenantRoot, args, {
|
|
|
201
206
|
prefix
|
|
202
207
|
}) {
|
|
203
208
|
let lastOutput = "";
|
|
204
|
-
for (let attempt = 1; attempt <=
|
|
209
|
+
for (let attempt = 1; attempt <= WRANGLER_TRANSIENT_MAX_ATTEMPTS; attempt += 1) {
|
|
205
210
|
const wrangler = resolveTreeseedToolCommand("wrangler");
|
|
206
211
|
if (!wrangler) {
|
|
207
212
|
throw new Error("Wrangler CLI is unavailable.");
|
|
@@ -210,22 +215,24 @@ async function runPrefixedWranglerWithRetry(tenantRoot, args, {
|
|
|
210
215
|
cwd: tenantRoot,
|
|
211
216
|
env: env2,
|
|
212
217
|
write,
|
|
213
|
-
prefix
|
|
218
|
+
prefix,
|
|
219
|
+
timeoutMs: WRANGLER_COMMAND_TIMEOUT_MS
|
|
214
220
|
});
|
|
215
221
|
if (result.status === 0) {
|
|
216
222
|
return result;
|
|
217
223
|
}
|
|
218
224
|
lastOutput = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
|
|
219
|
-
if (attempt ===
|
|
225
|
+
if (attempt === WRANGLER_TRANSIENT_MAX_ATTEMPTS || !isTransientWranglerOutput(lastOutput)) {
|
|
220
226
|
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
221
227
|
}
|
|
228
|
+
const retryDelayMs = wranglerTransientRetryDelayMs(attempt);
|
|
222
229
|
writeTreeseedBootstrapLine(
|
|
223
230
|
write,
|
|
224
231
|
{ ...prefix, stage: "retry" },
|
|
225
|
-
`Wrangler command hit a transient failure; retrying in ${
|
|
232
|
+
`Wrangler command hit a transient failure; retrying in ${Math.round(retryDelayMs / 1e3)}s...`,
|
|
226
233
|
"stderr"
|
|
227
234
|
);
|
|
228
|
-
await sleep(
|
|
235
|
+
await sleep(retryDelayMs);
|
|
229
236
|
}
|
|
230
237
|
throw new Error(lastOutput || `wrangler ${args.join(" ")} failed`);
|
|
231
238
|
}
|
|
@@ -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:
|
|
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:
|
|
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
|
|
5391
|
-
const
|
|
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