@treeseed/sdk 0.5.2 → 0.6.0

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.
Files changed (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +465 -142
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2093 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. package/package.json +7 -1
@@ -32,6 +32,15 @@ export declare function createFeatureBranchFromStaging(cwd: any, branchName: any
32
32
  export declare function pushBranch(repoDir: any, branchName: any, { setUpstream }?: {
33
33
  setUpstream?: boolean | undefined;
34
34
  }): void;
35
+ export declare function ensureRemoteBranchFromBase(repoDir: any, branchName: any, { baseBranch }?: {
36
+ baseBranch?: string | undefined;
37
+ }): {
38
+ branchName: any;
39
+ baseBranch: string;
40
+ createdLocal: boolean;
41
+ pushed: boolean;
42
+ existed: boolean;
43
+ };
35
44
  export declare function deleteLocalBranch(repoDir: any, branchName: any): void;
36
45
  export declare function deleteRemoteBranch(repoDir: any, branchName: any): boolean;
37
46
  export declare function mergeCurrentBranchIntoStaging(cwd: any, featureBranch: any): {
@@ -147,6 +147,37 @@ function pushBranch(repoDir, branchName, { setUpstream = false } = {}) {
147
147
  const args = setUpstream ? ["push", "-u", "origin", branchName] : ["push", "origin", branchName];
148
148
  runGit(args, { cwd: repoDir });
149
149
  }
150
+ function ensureRemoteBranchFromBase(repoDir, branchName, { baseBranch = PRODUCTION_BRANCH } = {}) {
151
+ fetchOrigin(repoDir);
152
+ if (remoteBranchExists(repoDir, branchName)) {
153
+ return {
154
+ branchName,
155
+ baseBranch,
156
+ createdLocal: branchExists(repoDir, branchName) ? false : (() => {
157
+ runGit(["branch", branchName, `origin/${branchName}`], { cwd: repoDir });
158
+ return true;
159
+ })(),
160
+ pushed: false,
161
+ existed: true
162
+ };
163
+ }
164
+ const baseRef = remoteBranchExists(repoDir, baseBranch) ? `origin/${baseBranch}` : branchExists(repoDir, baseBranch) ? baseBranch : "";
165
+ if (!baseRef) {
166
+ throw new Error(`Base branch "${baseBranch}" does not exist locally or on origin.`);
167
+ }
168
+ const createdLocal = !branchExists(repoDir, branchName);
169
+ if (createdLocal) {
170
+ runGit(["branch", branchName, baseRef], { cwd: repoDir });
171
+ }
172
+ pushBranch(repoDir, branchName, { setUpstream: true });
173
+ return {
174
+ branchName,
175
+ baseBranch,
176
+ createdLocal,
177
+ pushed: true,
178
+ existed: false
179
+ };
180
+ }
150
181
  function deleteLocalBranch(repoDir, branchName) {
151
182
  if (!branchExists(repoDir, branchName)) {
152
183
  return;
@@ -306,6 +337,7 @@ export {
306
337
  deleteLocalBranch,
307
338
  deleteRemoteBranch,
308
339
  ensureLocalBranchTracking,
340
+ ensureRemoteBranchFromBase,
309
341
  fetchOrigin,
310
342
  gitWorkflowRoot,
311
343
  headCommit,
@@ -0,0 +1,115 @@
1
+ import { Octokit } from 'octokit';
2
+ export type GitHubApiClient = Octokit;
3
+ export interface GitHubRepositoryMetadataInput {
4
+ owner: string;
5
+ name: string;
6
+ description?: string | null;
7
+ homepageUrl?: string | null;
8
+ visibility?: 'private' | 'public' | 'internal';
9
+ topics?: string[];
10
+ }
11
+ export interface GitHubRepositorySummary {
12
+ id: number;
13
+ owner: string;
14
+ name: string;
15
+ slug: string;
16
+ url: string;
17
+ sshUrl: string;
18
+ httpsUrl: string;
19
+ defaultBranch: string;
20
+ visibility: 'private' | 'public' | 'internal';
21
+ }
22
+ export interface GitHubWorkflowRunSummary {
23
+ id: number;
24
+ status: string | null;
25
+ conclusion: string | null;
26
+ url: string | null;
27
+ headSha: string | null;
28
+ headBranch: string | null;
29
+ }
30
+ export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
31
+ export declare function parseGitHubRepositorySlug(value: string): {
32
+ owner: string;
33
+ name: string;
34
+ };
35
+ export declare function createGitHubApiClient({ env, timeoutMs, }?: {
36
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
37
+ timeoutMs?: number;
38
+ }): GitHubApiClient;
39
+ export declare function getGitHubRepository(repository: string | {
40
+ owner: string;
41
+ name: string;
42
+ }, { client }?: {
43
+ client?: GitHubApiClient;
44
+ }): Promise<GitHubRepositorySummary>;
45
+ export declare function maybeGetGitHubRepository(repository: string | {
46
+ owner: string;
47
+ name: string;
48
+ }, { client }?: {
49
+ client?: GitHubApiClient;
50
+ }): Promise<GitHubRepositorySummary | null>;
51
+ export declare function ensureGitHubRepository(input: GitHubRepositoryMetadataInput, { client }?: {
52
+ client?: GitHubApiClient;
53
+ }): Promise<GitHubRepositorySummary>;
54
+ export declare function listGitHubRepositorySecretNames(repository: string | {
55
+ owner: string;
56
+ name: string;
57
+ }, { client }?: {
58
+ client?: GitHubApiClient;
59
+ }): Promise<Set<string>>;
60
+ export declare function listGitHubRepositoryVariableNames(repository: string | {
61
+ owner: string;
62
+ name: string;
63
+ }, { client }?: {
64
+ client?: GitHubApiClient;
65
+ }): Promise<Set<string>>;
66
+ export declare function upsertGitHubRepositorySecret(repository: string | {
67
+ owner: string;
68
+ name: string;
69
+ }, name: string, value: string, { client }?: {
70
+ client?: GitHubApiClient;
71
+ }): Promise<void>;
72
+ export declare function upsertGitHubRepositoryVariable(repository: string | {
73
+ owner: string;
74
+ name: string;
75
+ }, name: string, value: string, { client }?: {
76
+ client?: GitHubApiClient;
77
+ }): Promise<void>;
78
+ export declare function upsertGitHubRepositoryVariableWithGhCli(repository: string | {
79
+ owner: string;
80
+ name: string;
81
+ }, name: string, value: string, { env, }?: {
82
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
83
+ }): void;
84
+ export declare function waitForGitHubWorkflowRunCompletion(repository: string | {
85
+ owner: string;
86
+ name: string;
87
+ }, { client, workflow, headSha, branch, timeoutSeconds, pollSeconds, }?: {
88
+ client?: GitHubApiClient;
89
+ workflow?: string;
90
+ headSha?: string | null;
91
+ branch?: string | null;
92
+ timeoutSeconds?: number;
93
+ pollSeconds?: number;
94
+ }): Promise<{
95
+ status: string;
96
+ repository: string;
97
+ workflow: string;
98
+ runId: number;
99
+ headSha: string | null;
100
+ conclusion: string | null;
101
+ url: string | null;
102
+ }>;
103
+ export declare function ensureGitHubBranchFromBase(repository: string | {
104
+ owner: string;
105
+ name: string;
106
+ }, branch: string, { baseBranch, client, }?: {
107
+ baseBranch?: string;
108
+ client?: GitHubApiClient;
109
+ }): Promise<{
110
+ branch: string;
111
+ baseBranch: string;
112
+ existed: boolean;
113
+ created: boolean;
114
+ sha: string;
115
+ }>;
@@ -0,0 +1,455 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { spawnSync } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+ import { Octokit } from "octokit";
5
+ const require2 = createRequire(import.meta.url);
6
+ const sodium = require2("libsodium-wrappers");
7
+ const DEFAULT_GITHUB_API_TIMEOUT_MS = 6e4;
8
+ function normalizeGitHubVisibility(value, fallback = "private") {
9
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
10
+ return normalized === "public" || normalized === "internal" || normalized === "private" ? normalized : fallback;
11
+ }
12
+ function configuredEnvValue(env, key) {
13
+ const value = env?.[key];
14
+ return typeof value === "string" && value.trim() ? value.trim() : "";
15
+ }
16
+ function resolveGitHubApiToken(env = process.env) {
17
+ return configuredEnvValue(env, "GH_TOKEN") || configuredEnvValue(env, "GITHUB_TOKEN");
18
+ }
19
+ function parseGitHubRepositorySlug(value) {
20
+ const normalized = String(value ?? "").trim().replace(/\.git$/u, "");
21
+ const [owner, ...rest] = normalized.split("/").filter(Boolean);
22
+ if (!owner || rest.length === 0) {
23
+ throw new Error(`Invalid GitHub repository slug "${value}". Expected owner/name.`);
24
+ }
25
+ return {
26
+ owner,
27
+ name: rest.join("/")
28
+ };
29
+ }
30
+ function createGitHubSignal(timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS) {
31
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
32
+ return AbortSignal.timeout(timeoutMs);
33
+ }
34
+ return void 0;
35
+ }
36
+ function normalizeGitHubApiError(error, context) {
37
+ if (error && typeof error === "object") {
38
+ const status = typeof error.status === "number" ? error.status : null;
39
+ const message = typeof error.message === "string" ? error.message.trim() : "";
40
+ if (status === 401 || status === 403) {
41
+ return new Error(`${context}: GitHub authentication failed.`);
42
+ }
43
+ if (status === 404) {
44
+ return new Error(`${context}: GitHub resource was not found.`);
45
+ }
46
+ if (status === 422) {
47
+ return new Error(`${context}: ${message || "GitHub rejected the request."}`);
48
+ }
49
+ if (status && message) {
50
+ return new Error(`${context}: ${message}`);
51
+ }
52
+ }
53
+ if (error instanceof Error && error.message.trim()) {
54
+ return new Error(`${context}: ${error.message.trim()}`);
55
+ }
56
+ return new Error(`${context}: GitHub API request failed.`);
57
+ }
58
+ function isRetriableGitHubApiError(error) {
59
+ const message = error instanceof Error ? error.message : String(error ?? "");
60
+ return /timed out|timeout|aborted|ECONNRESET|ECONNREFUSED|fetch failed|socket hang up/iu.test(message);
61
+ }
62
+ async function withGitHubApiRetries(operation, retries = 2) {
63
+ let attempt = 0;
64
+ let lastError;
65
+ while (attempt <= retries) {
66
+ try {
67
+ return await operation();
68
+ } catch (error) {
69
+ lastError = error;
70
+ if (attempt >= retries || !isRetriableGitHubApiError(error)) {
71
+ throw error;
72
+ }
73
+ await sleep(1e3 * (attempt + 1));
74
+ attempt += 1;
75
+ }
76
+ }
77
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "GitHub API request failed."));
78
+ }
79
+ function normalizeRepositorySummary(repository) {
80
+ return {
81
+ id: Number(repository.id ?? 0),
82
+ owner: String(repository.owner?.login ?? repository.owner?.name ?? ""),
83
+ name: String(repository.name ?? ""),
84
+ slug: `${String(repository.owner?.login ?? repository.owner?.name ?? "")}/${String(repository.name ?? "")}`,
85
+ url: String(repository.html_url ?? repository.url ?? ""),
86
+ sshUrl: String(repository.ssh_url ?? ""),
87
+ httpsUrl: String(repository.clone_url ?? ""),
88
+ defaultBranch: String(repository.default_branch ?? "main"),
89
+ visibility: normalizeGitHubVisibility(repository.visibility, repository.private ? "private" : "public")
90
+ };
91
+ }
92
+ function createGitHubApiClient({
93
+ env = process.env,
94
+ timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS
95
+ } = {}) {
96
+ const token = resolveGitHubApiToken(env);
97
+ if (!token) {
98
+ throw new Error("Configure GH_TOKEN before using Treeseed GitHub automation.");
99
+ }
100
+ return new Octokit({
101
+ auth: token,
102
+ request: {
103
+ signal: createGitHubSignal(timeoutMs)
104
+ }
105
+ });
106
+ }
107
+ async function getGitHubRepository(repository, { client = createGitHubApiClient() } = {}) {
108
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
109
+ try {
110
+ const response = await client.rest.repos.get({ owner, repo: name });
111
+ return normalizeRepositorySummary(response.data);
112
+ } catch (error) {
113
+ throw normalizeGitHubApiError(error, `Unable to load GitHub repository ${owner}/${name}`);
114
+ }
115
+ }
116
+ async function maybeGetGitHubRepository(repository, { client = createGitHubApiClient() } = {}) {
117
+ try {
118
+ return await getGitHubRepository(repository, { client });
119
+ } catch (error) {
120
+ if (/not found/iu.test(error instanceof Error ? error.message : String(error ?? ""))) {
121
+ return null;
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+ async function ensureGitHubRepository(input, { client = createGitHubApiClient() } = {}) {
127
+ const existing = await maybeGetGitHubRepository({ owner: input.owner, name: input.name }, { client });
128
+ let repository = existing;
129
+ if (!repository) {
130
+ try {
131
+ const viewer = await client.rest.users.getAuthenticated();
132
+ if (viewer.data.login === input.owner) {
133
+ const created = await client.rest.repos.createForAuthenticatedUser({
134
+ name: input.name,
135
+ description: input.description ?? void 0,
136
+ homepage: input.homepageUrl ?? void 0,
137
+ private: (input.visibility ?? "private") !== "public"
138
+ });
139
+ repository = normalizeRepositorySummary(created.data);
140
+ } else {
141
+ const created = await client.rest.repos.createInOrg({
142
+ org: input.owner,
143
+ name: input.name,
144
+ description: input.description ?? void 0,
145
+ homepage: input.homepageUrl ?? void 0,
146
+ visibility: input.visibility ?? "private"
147
+ });
148
+ repository = normalizeRepositorySummary(created.data);
149
+ }
150
+ } catch (error) {
151
+ throw normalizeGitHubApiError(error, `Unable to create GitHub repository ${input.owner}/${input.name}`);
152
+ }
153
+ }
154
+ try {
155
+ const updated = await client.rest.repos.update({
156
+ owner: input.owner,
157
+ repo: input.name,
158
+ name: input.name,
159
+ description: input.description ?? void 0,
160
+ homepage: input.homepageUrl ?? void 0,
161
+ private: (input.visibility ?? repository.visibility ?? "private") === "private",
162
+ visibility: input.visibility ?? repository.visibility
163
+ });
164
+ repository = normalizeRepositorySummary(updated.data);
165
+ if (Array.isArray(input.topics)) {
166
+ await client.rest.repos.replaceAllTopics({
167
+ owner: input.owner,
168
+ repo: input.name,
169
+ names: input.topics
170
+ });
171
+ }
172
+ return repository;
173
+ } catch (error) {
174
+ throw normalizeGitHubApiError(error, `Unable to update GitHub repository ${input.owner}/${input.name}`);
175
+ }
176
+ }
177
+ async function paginateNames(request) {
178
+ const items = await request();
179
+ return new Set(
180
+ items.map((entry) => typeof entry?.name === "string" ? entry.name.trim() : "").filter(Boolean)
181
+ );
182
+ }
183
+ async function listGitHubRepositorySecretNames(repository, { client = createGitHubApiClient() } = {}) {
184
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
185
+ try {
186
+ return await paginateNames(
187
+ () => client.paginate(client.rest.actions.listRepoSecrets, {
188
+ owner,
189
+ repo: name,
190
+ per_page: 100
191
+ })
192
+ );
193
+ } catch (error) {
194
+ throw normalizeGitHubApiError(error, `Unable to list GitHub secrets for ${owner}/${name}`);
195
+ }
196
+ }
197
+ async function listGitHubRepositoryVariableNames(repository, { client = createGitHubApiClient() } = {}) {
198
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
199
+ try {
200
+ return await paginateNames(
201
+ () => client.paginate(client.rest.actions.listRepoVariables, {
202
+ owner,
203
+ repo: name,
204
+ per_page: 100
205
+ })
206
+ );
207
+ } catch (error) {
208
+ throw normalizeGitHubApiError(error, `Unable to list GitHub variables for ${owner}/${name}`);
209
+ }
210
+ }
211
+ async function encryptGitHubSecret(secret, key) {
212
+ await sodium.ready;
213
+ const messageBytes = Buffer.from(secret);
214
+ const keyBytes = Buffer.from(key, "base64");
215
+ return Buffer.from(sodium.crypto_box_seal(messageBytes, keyBytes)).toString("base64");
216
+ }
217
+ async function upsertGitHubRepositorySecret(repository, name, value, { client = createGitHubApiClient() } = {}) {
218
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
219
+ try {
220
+ const key = await client.rest.actions.getRepoPublicKey({
221
+ owner,
222
+ repo
223
+ });
224
+ const encryptedValue = await encryptGitHubSecret(value, key.data.key);
225
+ await withGitHubApiRetries(() => client.rest.actions.createOrUpdateRepoSecret({
226
+ owner,
227
+ repo,
228
+ secret_name: name,
229
+ encrypted_value: encryptedValue,
230
+ key_id: key.data.key_id
231
+ }));
232
+ } catch (error) {
233
+ throw normalizeGitHubApiError(error, `Unable to upsert GitHub secret ${name} for ${owner}/${repo}`);
234
+ }
235
+ }
236
+ async function upsertGitHubRepositoryVariable(repository, name, value, { client = createGitHubApiClient() } = {}) {
237
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
238
+ try {
239
+ const createOrUpdateRepoVariable = client.rest.actions.createOrUpdateRepoVariable;
240
+ if (typeof createOrUpdateRepoVariable === "function") {
241
+ await withGitHubApiRetries(() => createOrUpdateRepoVariable({
242
+ owner,
243
+ repo,
244
+ name,
245
+ value
246
+ }));
247
+ return;
248
+ }
249
+ await withGitHubApiRetries(async () => {
250
+ try {
251
+ await client.request("POST /repos/{owner}/{repo}/actions/variables", {
252
+ owner,
253
+ repo,
254
+ name,
255
+ value
256
+ });
257
+ return;
258
+ } catch (error) {
259
+ const status = typeof error?.status === "number" ? Number(error.status) : null;
260
+ const message = error instanceof Error ? error.message : String(error ?? "");
261
+ if (status !== 409 && status !== 422 && !/already exists/iu.test(message)) {
262
+ throw error;
263
+ }
264
+ }
265
+ await client.request("PATCH /repos/{owner}/{repo}/actions/variables/{name}", {
266
+ owner,
267
+ repo,
268
+ name,
269
+ value
270
+ });
271
+ });
272
+ } catch (error) {
273
+ throw normalizeGitHubApiError(error, `Unable to upsert GitHub variable ${name} for ${owner}/${repo}`);
274
+ }
275
+ }
276
+ function upsertGitHubRepositoryVariableWithGhCli(repository, name, value, {
277
+ env = process.env
278
+ } = {}) {
279
+ const token = resolveGitHubApiToken(env);
280
+ if (!token) {
281
+ throw new Error("Configure GH_TOKEN before using Treeseed GitHub automation.");
282
+ }
283
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
284
+ const ghEnv = {
285
+ ...process.env,
286
+ ...env,
287
+ GH_TOKEN: token,
288
+ GITHUB_TOKEN: token
289
+ };
290
+ const create = spawnSync(
291
+ "gh",
292
+ [
293
+ "api",
294
+ `repos/${owner}/${repo}/actions/variables`,
295
+ "--method",
296
+ "POST",
297
+ "-f",
298
+ `name=${name}`,
299
+ "-f",
300
+ `value=${value}`
301
+ ],
302
+ { encoding: "utf8", env: ghEnv }
303
+ );
304
+ if (create.status === 0) {
305
+ return;
306
+ }
307
+ const combinedCreateOutput = `${create.stdout ?? ""}
308
+ ${create.stderr ?? ""}`.trim();
309
+ if (!/already exists|HTTP 409|HTTP 422/iu.test(combinedCreateOutput)) {
310
+ throw new Error(combinedCreateOutput || `gh api exited with status ${create.status ?? 1}`);
311
+ }
312
+ const update = spawnSync(
313
+ "gh",
314
+ [
315
+ "api",
316
+ `repos/${owner}/${repo}/actions/variables/${name}`,
317
+ "--method",
318
+ "PATCH",
319
+ "-f",
320
+ `name=${name}`,
321
+ "-f",
322
+ `value=${value}`
323
+ ],
324
+ { encoding: "utf8", env: ghEnv }
325
+ );
326
+ if (update.status === 0) {
327
+ return;
328
+ }
329
+ const combinedUpdateOutput = `${update.stdout ?? ""}
330
+ ${update.stderr ?? ""}`.trim();
331
+ throw new Error(combinedUpdateOutput || `gh api exited with status ${update.status ?? 1}`);
332
+ }
333
+ function normalizeWorkflowRun(run) {
334
+ return {
335
+ id: Number(run.id ?? 0),
336
+ status: typeof run.status === "string" ? run.status : null,
337
+ conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
338
+ url: typeof run.html_url === "string" ? run.html_url : null,
339
+ headSha: typeof run.head_sha === "string" ? run.head_sha : null,
340
+ headBranch: typeof run.head_branch === "string" ? run.head_branch : null
341
+ };
342
+ }
343
+ function sleep(ms) {
344
+ return new Promise((resolve) => setTimeout(resolve, ms));
345
+ }
346
+ async function waitForGitHubWorkflowRunCompletion(repository, {
347
+ client = createGitHubApiClient(),
348
+ workflow = "publish.yml",
349
+ headSha,
350
+ branch,
351
+ timeoutSeconds = 600,
352
+ pollSeconds = 5
353
+ } = {}) {
354
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
355
+ const startedAt = Date.now();
356
+ while (Date.now() - startedAt < timeoutSeconds * 1e3) {
357
+ try {
358
+ const listed = await client.rest.actions.listWorkflowRuns({
359
+ owner,
360
+ repo: name,
361
+ workflow_id: workflow,
362
+ per_page: 20
363
+ });
364
+ const match = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).find((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
365
+ if (!match?.id) {
366
+ await sleep(pollSeconds * 1e3);
367
+ continue;
368
+ }
369
+ for (; ; ) {
370
+ const current = await client.rest.actions.getWorkflowRun({
371
+ owner,
372
+ repo: name,
373
+ run_id: match.id
374
+ });
375
+ const normalized = normalizeWorkflowRun(current.data);
376
+ if (normalized.status === "completed") {
377
+ return {
378
+ status: "completed",
379
+ repository: `${owner}/${name}`,
380
+ workflow,
381
+ runId: normalized.id,
382
+ headSha: normalized.headSha,
383
+ conclusion: normalized.conclusion,
384
+ url: normalized.url
385
+ };
386
+ }
387
+ await sleep(pollSeconds * 1e3);
388
+ }
389
+ } catch (error) {
390
+ throw normalizeGitHubApiError(error, `Unable to monitor GitHub workflow ${workflow} in ${owner}/${name}`);
391
+ }
392
+ }
393
+ throw new Error(`Timed out waiting for GitHub workflow ${workflow} in ${owner}/${name}.`);
394
+ }
395
+ async function ensureGitHubBranchFromBase(repository, branch, {
396
+ baseBranch = "main",
397
+ client = createGitHubApiClient()
398
+ } = {}) {
399
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
400
+ try {
401
+ const existing = await client.rest.repos.getBranch({
402
+ owner,
403
+ repo: name,
404
+ branch
405
+ });
406
+ return {
407
+ branch,
408
+ baseBranch,
409
+ existed: true,
410
+ created: false,
411
+ sha: existing.data.commit.sha
412
+ };
413
+ } catch (error) {
414
+ if (!/not found/iu.test(error instanceof Error ? error.message : String(error ?? ""))) {
415
+ throw normalizeGitHubApiError(error, `Unable to resolve GitHub branch ${branch} in ${owner}/${name}`);
416
+ }
417
+ }
418
+ try {
419
+ const base = await client.rest.repos.getBranch({
420
+ owner,
421
+ repo: name,
422
+ branch: baseBranch
423
+ });
424
+ await client.rest.git.createRef({
425
+ owner,
426
+ repo: name,
427
+ ref: `refs/heads/${branch}`,
428
+ sha: base.data.commit.sha
429
+ });
430
+ return {
431
+ branch,
432
+ baseBranch,
433
+ existed: false,
434
+ created: true,
435
+ sha: base.data.commit.sha
436
+ };
437
+ } catch (error) {
438
+ throw normalizeGitHubApiError(error, `Unable to create GitHub branch ${branch} from ${baseBranch} in ${owner}/${name}`);
439
+ }
440
+ }
441
+ export {
442
+ createGitHubApiClient,
443
+ ensureGitHubBranchFromBase,
444
+ ensureGitHubRepository,
445
+ getGitHubRepository,
446
+ listGitHubRepositorySecretNames,
447
+ listGitHubRepositoryVariableNames,
448
+ maybeGetGitHubRepository,
449
+ parseGitHubRepositorySlug,
450
+ resolveGitHubApiToken,
451
+ upsertGitHubRepositorySecret,
452
+ upsertGitHubRepositoryVariable,
453
+ upsertGitHubRepositoryVariableWithGhCli,
454
+ waitForGitHubWorkflowRunCompletion
455
+ };