@treeseed/sdk 0.5.3 → 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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/operations/providers/default.js +1 -1
- package/dist/operations/services/config-runtime.d.ts +49 -42
- package/dist/operations/services/config-runtime.js +449 -136
- package/dist/operations/services/deploy.d.ts +298 -0
- package/dist/operations/services/deploy.js +381 -137
- package/dist/operations/services/git-workflow.d.ts +9 -0
- package/dist/operations/services/git-workflow.js +32 -0
- package/dist/operations/services/github-api.d.ts +115 -0
- package/dist/operations/services/github-api.js +455 -0
- package/dist/operations/services/github-automation.d.ts +19 -33
- package/dist/operations/services/github-automation.js +44 -131
- package/dist/operations/services/key-agent.d.ts +20 -1
- package/dist/operations/services/key-agent.js +267 -102
- package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
- package/dist/operations/services/knowledge-coop-launch.js +26 -12
- package/dist/operations/services/project-platform.d.ts +157 -150
- package/dist/operations/services/project-platform.js +129 -26
- package/dist/operations/services/railway-api.d.ts +244 -0
- package/dist/operations/services/railway-api.js +882 -0
- package/dist/operations/services/railway-deploy.d.ts +171 -27
- package/dist/operations/services/railway-deploy.js +672 -172
- package/dist/operations/services/runtime-tools.d.ts +18 -0
- package/dist/operations/services/runtime-tools.js +19 -6
- package/dist/operations/services/workspace-preflight.js +2 -2
- package/dist/platform/contracts.d.ts +7 -0
- package/dist/platform/deploy-config.js +23 -0
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +7 -9
- package/dist/platform/env.yaml +10 -9
- package/dist/platform/environment.js +4 -0
- package/dist/platform/plugin.d.ts +6 -0
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +1 -0
- package/dist/platform/plugins/runtime.d.ts +4 -0
- package/dist/platform/plugins/runtime.js +8 -1
- package/dist/platform/published-content.js +27 -4
- package/dist/platform/tenant/runtime-config.js +33 -24
- package/dist/plugin-default.d.ts +1 -0
- package/dist/plugin-default.js +1 -0
- package/dist/reconcile/builtin-adapters.d.ts +3 -0
- package/dist/reconcile/builtin-adapters.js +2093 -0
- package/dist/reconcile/contracts.d.ts +155 -0
- package/dist/reconcile/contracts.js +0 -0
- package/dist/reconcile/desired-state.d.ts +179 -0
- package/dist/reconcile/desired-state.js +319 -0
- package/dist/reconcile/engine.d.ts +405 -0
- package/dist/reconcile/engine.js +356 -0
- package/dist/reconcile/errors.d.ts +5 -0
- package/dist/reconcile/errors.js +13 -0
- package/dist/reconcile/index.d.ts +7 -0
- package/dist/reconcile/index.js +7 -0
- package/dist/reconcile/registry.d.ts +7 -0
- package/dist/reconcile/registry.js +64 -0
- package/dist/reconcile/state.d.ts +7 -0
- package/dist/reconcile/state.js +303 -0
- package/dist/reconcile/units.d.ts +6 -0
- package/dist/reconcile/units.js +68 -0
- package/dist/scripts/config-treeseed.js +27 -19
- package/dist/scripts/tenant-deploy.js +35 -14
- package/dist/workflow/operations.js +127 -22
- package/dist/workflow-support.d.ts +3 -1
- package/dist/workflow-support.js +50 -0
- package/dist/workflow.d.ts +2 -0
- 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
|
+
};
|