@realiizlabs/admin 0.1.1 → 0.7.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/auth/index.cjs +182 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +178 -0
- package/dist/auth/index.d.ts +178 -0
- package/dist/auth/index.js +167 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-ui/index.cjs +124 -0
- package/dist/auth-ui/index.cjs.map +1 -0
- package/dist/auth-ui/index.d.cts +40 -0
- package/dist/auth-ui/index.d.ts +40 -0
- package/dist/auth-ui/index.js +81 -0
- package/dist/auth-ui/index.js.map +1 -0
- package/dist/chunk-4OCBMOEP.js +42 -0
- package/dist/chunk-4OCBMOEP.js.map +1 -0
- package/dist/chunk-IS52OXZ2.js +252 -0
- package/dist/chunk-IS52OXZ2.js.map +1 -0
- package/dist/forms/index.cjs +256 -0
- package/dist/forms/index.cjs.map +1 -0
- package/dist/forms/index.d.cts +48 -0
- package/dist/forms/index.d.ts +48 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms-ui/index.cjs +584 -0
- package/dist/forms-ui/index.cjs.map +1 -0
- package/dist/forms-ui/index.d.cts +33 -0
- package/dist/forms-ui/index.d.ts +33 -0
- package/dist/forms-ui/index.js +336 -0
- package/dist/forms-ui/index.js.map +1 -0
- package/dist/git/index.cjs +283 -0
- package/dist/git/index.cjs.map +1 -0
- package/dist/git/index.d.cts +258 -0
- package/dist/git/index.d.ts +258 -0
- package/dist/git/index.js +269 -0
- package/dist/git/index.js.map +1 -0
- package/dist/types-DsSMscTh.d.cts +74 -0
- package/dist/types-DsSMscTh.d.ts +74 -0
- package/package.json +55 -3
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('server-only');
|
|
4
|
+
|
|
5
|
+
// src/git/index.ts
|
|
6
|
+
|
|
7
|
+
// src/git/errors.ts
|
|
8
|
+
var GitHubError = class extends Error {
|
|
9
|
+
constructor(message, opts) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "GitHubError";
|
|
12
|
+
this.status = opts.status;
|
|
13
|
+
this.path = opts.path;
|
|
14
|
+
this.body = opts.body;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var StaleBaseError = class extends GitHubError {
|
|
18
|
+
constructor(pullNumber, opts) {
|
|
19
|
+
super(`Pull request #${pullNumber} conflicts with the base branch`, opts);
|
|
20
|
+
this.name = "StaleBaseError";
|
|
21
|
+
this.pullNumber = pullNumber;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var _TokenScopeError = class _TokenScopeError extends GitHubError {
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
super(
|
|
27
|
+
`GitHub refused ${opts.path} (${opts.status}). The repo token needs these fine-grained permissions: ${_TokenScopeError.REQUIRED_PERMISSIONS}`,
|
|
28
|
+
opts
|
|
29
|
+
);
|
|
30
|
+
this.name = "TokenScopeError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
_TokenScopeError.REQUIRED_PERMISSIONS = "Contents: Read and write, Pull requests: Read and write, Commit statuses: Read-only";
|
|
34
|
+
var TokenScopeError = _TokenScopeError;
|
|
35
|
+
var ChecksFailedError = class extends Error {
|
|
36
|
+
constructor(pullNumber, failingCheck) {
|
|
37
|
+
super(
|
|
38
|
+
`Pull request #${pullNumber} has failing checks${failingCheck ? ` (${failingCheck})` : ""}`
|
|
39
|
+
);
|
|
40
|
+
this.name = "ChecksFailedError";
|
|
41
|
+
this.pullNumber = pullNumber;
|
|
42
|
+
this.failingCheck = failingCheck;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/git/client.ts
|
|
47
|
+
var API = "https://api.github.com";
|
|
48
|
+
var GITHUB_TIMEOUT_MS = 1e4;
|
|
49
|
+
async function ghFetch(ctx, method, path, body) {
|
|
50
|
+
const res = await fetch(API + path, {
|
|
51
|
+
method,
|
|
52
|
+
signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
55
|
+
Accept: "application/vnd.github+json",
|
|
56
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
57
|
+
"User-Agent": "realiizlabs-admin",
|
|
58
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
59
|
+
},
|
|
60
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
61
|
+
});
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
let json = null;
|
|
64
|
+
if (text) {
|
|
65
|
+
try {
|
|
66
|
+
json = JSON.parse(text);
|
|
67
|
+
} catch {
|
|
68
|
+
json = { raw: text };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ok: res.ok, status: res.status, json };
|
|
72
|
+
}
|
|
73
|
+
async function ghFetchOrThrow(ctx, method, path, body) {
|
|
74
|
+
const res = await ghFetch(ctx, method, path, body);
|
|
75
|
+
if (!res.ok) throw toError(res, path);
|
|
76
|
+
return res.json;
|
|
77
|
+
}
|
|
78
|
+
function toError(res, path) {
|
|
79
|
+
const opts = { status: res.status, path, body: res.json };
|
|
80
|
+
if (res.status === 403) return new TokenScopeError(opts);
|
|
81
|
+
const message = res.json?.message ?? `GitHub ${res.status} on ${path}`;
|
|
82
|
+
return new GitHubError(message, opts);
|
|
83
|
+
}
|
|
84
|
+
function repoPath(ctx) {
|
|
85
|
+
return `/repos/${encodeURIComponent(ctx.owner)}/${encodeURIComponent(ctx.repo)}`;
|
|
86
|
+
}
|
|
87
|
+
function baseBranch(ctx) {
|
|
88
|
+
return ctx.baseBranch ?? "main";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/git/branch.ts
|
|
92
|
+
async function getBranchSha(ctx, branch) {
|
|
93
|
+
const ref = await ghFetchOrThrow(
|
|
94
|
+
ctx,
|
|
95
|
+
"GET",
|
|
96
|
+
`${repoPath(ctx)}/git/ref/heads/${encodeURIComponent(branch)}`
|
|
97
|
+
);
|
|
98
|
+
return ref.object.sha;
|
|
99
|
+
}
|
|
100
|
+
async function createBranch(ctx, branch) {
|
|
101
|
+
const baseSha = await getBranchSha(ctx, baseBranch(ctx));
|
|
102
|
+
await ghFetchOrThrow(ctx, "POST", `${repoPath(ctx)}/git/refs`, {
|
|
103
|
+
ref: `refs/heads/${branch}`,
|
|
104
|
+
sha: baseSha
|
|
105
|
+
});
|
|
106
|
+
return { branch, baseSha };
|
|
107
|
+
}
|
|
108
|
+
async function deleteBranch(ctx, branch) {
|
|
109
|
+
await ghFetchOrThrow(
|
|
110
|
+
ctx,
|
|
111
|
+
"DELETE",
|
|
112
|
+
`${repoPath(ctx)}/git/refs/heads/${encodeURIComponent(branch)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/git/files.ts
|
|
117
|
+
async function writeFiles(ctx, branch, files, message) {
|
|
118
|
+
if (files.length === 0) throw new Error("writeFiles: no files given");
|
|
119
|
+
const base = repoPath(ctx);
|
|
120
|
+
const parentSha = await getBranchSha(ctx, branch);
|
|
121
|
+
const tree = [];
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
if (file.encoding === "base64") {
|
|
124
|
+
const blob = await ghFetchOrThrow(ctx, "POST", `${base}/git/blobs`, {
|
|
125
|
+
content: file.content,
|
|
126
|
+
encoding: "base64"
|
|
127
|
+
});
|
|
128
|
+
tree.push({ path: file.path, mode: "100644", type: "blob", sha: blob.sha });
|
|
129
|
+
} else {
|
|
130
|
+
tree.push({ path: file.path, mode: "100644", type: "blob", content: file.content });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const newTree = await ghFetchOrThrow(ctx, "POST", `${base}/git/trees`, {
|
|
134
|
+
base_tree: parentSha,
|
|
135
|
+
tree
|
|
136
|
+
});
|
|
137
|
+
const commit = await ghFetchOrThrow(ctx, "POST", `${base}/git/commits`, {
|
|
138
|
+
message,
|
|
139
|
+
tree: newTree.sha,
|
|
140
|
+
parents: [parentSha]
|
|
141
|
+
});
|
|
142
|
+
await ghFetchOrThrow(
|
|
143
|
+
ctx,
|
|
144
|
+
"PATCH",
|
|
145
|
+
`${base}/git/refs/heads/${encodeURIComponent(branch)}`,
|
|
146
|
+
{ sha: commit.sha, force: false }
|
|
147
|
+
);
|
|
148
|
+
return { branch, commitSha: commit.sha, treeSha: newTree.sha, fileCount: files.length };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/git/pulls.ts
|
|
152
|
+
async function openPullRequest(ctx, branch, opts) {
|
|
153
|
+
const pr = await ghFetchOrThrow(ctx, "POST", `${repoPath(ctx)}/pulls`, {
|
|
154
|
+
title: opts.title,
|
|
155
|
+
head: branch,
|
|
156
|
+
base: baseBranch(ctx),
|
|
157
|
+
body: opts.body ?? ""
|
|
158
|
+
});
|
|
159
|
+
return { number: pr.number, html_url: pr.html_url, headSha: pr.head.sha, branch };
|
|
160
|
+
}
|
|
161
|
+
async function getPullRequest(ctx, number) {
|
|
162
|
+
return ghFetchOrThrow(ctx, "GET", `${repoPath(ctx)}/pulls/${number}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/git/status.ts
|
|
166
|
+
async function getPullRequestStatus(ctx, number, log = console) {
|
|
167
|
+
const pr = await getPullRequest(ctx, number);
|
|
168
|
+
if (pr.mergeable === null || pr.mergeable_state === "unknown") {
|
|
169
|
+
return { state: "pending" };
|
|
170
|
+
}
|
|
171
|
+
const combined = await ghFetchOrThrow(
|
|
172
|
+
ctx,
|
|
173
|
+
"GET",
|
|
174
|
+
`${repoPath(ctx)}/commits/${pr.head.sha}/status`
|
|
175
|
+
);
|
|
176
|
+
const failing = combined.statuses.find((s) => s.state === "failure" || s.state === "error");
|
|
177
|
+
if (pr.mergeable_state === "dirty") {
|
|
178
|
+
return { state: "red", failingCheck: failing?.context ?? "merge conflict" };
|
|
179
|
+
}
|
|
180
|
+
if (failing || pr.mergeable_state === "unstable") {
|
|
181
|
+
return { state: "red", failingCheck: failing?.context ?? null };
|
|
182
|
+
}
|
|
183
|
+
if (combined.total_count === 0) {
|
|
184
|
+
if (pr.mergeable_state === "clean" || pr.mergeable_state === "behind") {
|
|
185
|
+
const warning = `[admin/git] ${ctx.owner}/${ctx.repo} has no CI statuses on PR #${number} \u2014 treating as green. A client site with no CI is a setup bug.`;
|
|
186
|
+
log.warn(warning);
|
|
187
|
+
return { state: "green", warning };
|
|
188
|
+
}
|
|
189
|
+
return { state: "pending" };
|
|
190
|
+
}
|
|
191
|
+
if (combined.state === "success" && pr.mergeable_state !== "blocked") {
|
|
192
|
+
return { state: "green" };
|
|
193
|
+
}
|
|
194
|
+
return { state: "pending" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/git/merge.ts
|
|
198
|
+
async function mergePullRequest(ctx, number, opts = {}) {
|
|
199
|
+
const requireGreen = opts.requireGreen ?? true;
|
|
200
|
+
const path = `${repoPath(ctx)}/pulls/${number}/merge`;
|
|
201
|
+
if (requireGreen) {
|
|
202
|
+
const status = await getPullRequestStatus(ctx, number);
|
|
203
|
+
if (status.state === "pending") return { merged: false, state: "pending" };
|
|
204
|
+
if (status.state === "red") {
|
|
205
|
+
const pr = await getPullRequest(ctx, number);
|
|
206
|
+
if (pr.mergeable_state === "dirty") {
|
|
207
|
+
throw new StaleBaseError(number, { status: 0, path, body: pr });
|
|
208
|
+
}
|
|
209
|
+
throw new ChecksFailedError(number, status.failingCheck);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const res = await ghFetch(ctx, "PUT", path, {
|
|
213
|
+
merge_method: "squash",
|
|
214
|
+
...opts.commitTitle ? { commit_title: opts.commitTitle } : {}
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
if (res.status === 405 || res.status === 409) {
|
|
218
|
+
throw new StaleBaseError(number, { status: res.status, path, body: res.json });
|
|
219
|
+
}
|
|
220
|
+
throw toError(res, path);
|
|
221
|
+
}
|
|
222
|
+
if (opts.deleteBranch ?? true) {
|
|
223
|
+
const pr = await getPullRequest(ctx, number);
|
|
224
|
+
await deleteBranch(ctx, pr.head.ref);
|
|
225
|
+
}
|
|
226
|
+
return { merged: true, sha: res.json.sha };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/git/reads.ts
|
|
230
|
+
var NotFoundError = class extends GitHubError {
|
|
231
|
+
constructor(path) {
|
|
232
|
+
super(`Not found in repo: ${path}`, { status: 404, path, body: null });
|
|
233
|
+
this.name = "NotFoundError";
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function contentsPath(ctx, path, ref) {
|
|
237
|
+
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
238
|
+
const encoded = clean.split("/").map(encodeURIComponent).join("/");
|
|
239
|
+
return `${repoPath(ctx)}/contents/${encoded}?ref=${encodeURIComponent(ref ?? baseBranch(ctx))}`;
|
|
240
|
+
}
|
|
241
|
+
async function listDirectory(ctx, path, opts = {}) {
|
|
242
|
+
const url = contentsPath(ctx, path, opts.ref);
|
|
243
|
+
const res = await ghFetch(ctx, "GET", url);
|
|
244
|
+
if (res.status === 404) return [];
|
|
245
|
+
if (!res.ok) throw toError(res, url);
|
|
246
|
+
const list = Array.isArray(res.json) ? res.json : [];
|
|
247
|
+
return list.filter((e) => e.type === "file").map((e) => ({ name: e.name, path: e.path, sha: e.sha, size: e.size, type: "file" })).sort((a, b) => a.name.localeCompare(b.name));
|
|
248
|
+
}
|
|
249
|
+
async function readFile(ctx, path, opts = {}) {
|
|
250
|
+
const url = contentsPath(ctx, path, opts.ref);
|
|
251
|
+
const res = await ghFetch(ctx, "GET", url);
|
|
252
|
+
if (res.status === 404) throw new NotFoundError(path);
|
|
253
|
+
if (!res.ok) throw toError(res, url);
|
|
254
|
+
const entry = res.json;
|
|
255
|
+
if (Array.isArray(entry) || entry.type !== "file") throw new GitHubError(`${path} is not a file`, { status: res.status, path: url, body: entry });
|
|
256
|
+
if (typeof entry.content !== "string") {
|
|
257
|
+
throw new GitHubError(`${path} is too large to read through the Contents API`, { status: res.status, path: url, body: null });
|
|
258
|
+
}
|
|
259
|
+
const content = decodeBase64Utf8(entry.content);
|
|
260
|
+
return { path: entry.path, sha: entry.sha, content };
|
|
261
|
+
}
|
|
262
|
+
function decodeBase64Utf8(b64) {
|
|
263
|
+
const clean = b64.replace(/\s+/g, "");
|
|
264
|
+
const bin = atob(clean);
|
|
265
|
+
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
266
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exports.ChecksFailedError = ChecksFailedError;
|
|
270
|
+
exports.GitHubError = GitHubError;
|
|
271
|
+
exports.NotFoundError = NotFoundError;
|
|
272
|
+
exports.StaleBaseError = StaleBaseError;
|
|
273
|
+
exports.TokenScopeError = TokenScopeError;
|
|
274
|
+
exports.createBranch = createBranch;
|
|
275
|
+
exports.deleteBranch = deleteBranch;
|
|
276
|
+
exports.getPullRequestStatus = getPullRequestStatus;
|
|
277
|
+
exports.listDirectory = listDirectory;
|
|
278
|
+
exports.mergePullRequest = mergePullRequest;
|
|
279
|
+
exports.openPullRequest = openPullRequest;
|
|
280
|
+
exports.readFile = readFile;
|
|
281
|
+
exports.writeFiles = writeFiles;
|
|
282
|
+
//# sourceMappingURL=index.cjs.map
|
|
283
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/git/errors.ts","../../src/git/client.ts","../../src/git/branch.ts","../../src/git/files.ts","../../src/git/pulls.ts","../../src/git/status.ts","../../src/git/merge.ts","../../src/git/reads.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Typed errors — so ADMIN-04 can `instanceof` rather than string-match.\n *\n * Each carries enough to log usefully without ever including the token.\n */\n\n/** Any non-2xx from GitHub that is not one of the more specific cases below. */\nexport class GitHubError extends Error {\n readonly status: number;\n readonly path: string;\n readonly body: unknown;\n\n constructor(message: string, opts: { status: number; path: string; body: unknown }) {\n super(message);\n this.name = \"GitHubError\";\n this.status = opts.status;\n this.path = opts.path;\n this.body = opts.body;\n }\n}\n\n/**\n * The pull request cannot merge because `main` moved underneath it in a way\n * that conflicts — someone else edited the same file first.\n *\n * Observed in the probe: a stale base sha does NOT fail the trees write. Tree,\n * commit, ref and PR all succeed; the conflict only shows as\n * `mergeable_state: \"dirty\"` and then a 405 \"Pull Request has merge conflicts\"\n * on merge. So this is raised from mergePullRequest, never from writeFiles.\n * The recovery is the caller's: re-read main, rebuild the change, open a new PR.\n */\nexport class StaleBaseError extends GitHubError {\n readonly pullNumber: number;\n\n constructor(pullNumber: number, opts: { status: number; path: string; body: unknown }) {\n super(`Pull request #${pullNumber} conflicts with the base branch`, opts);\n this.name = \"StaleBaseError\";\n this.pullNumber = pullNumber;\n }\n}\n\n/**\n * The token cannot do what was asked. A 403 from GitHub with a fine-grained PAT\n * almost always means a permission was not granted when the token was made —\n * a setup bug at onboarding, not a runtime condition.\n */\nexport class TokenScopeError extends GitHubError {\n static readonly REQUIRED_PERMISSIONS =\n \"Contents: Read and write, Pull requests: Read and write, Commit statuses: Read-only\";\n\n constructor(opts: { status: number; path: string; body: unknown }) {\n super(\n `GitHub refused ${opts.path} (${opts.status}). The repo token needs these fine-grained permissions: ${TokenScopeError.REQUIRED_PERMISSIONS}`,\n opts,\n );\n this.name = \"TokenScopeError\";\n }\n}\n\n/** mergePullRequest was asked to merge while checks are red. */\nexport class ChecksFailedError extends Error {\n readonly pullNumber: number;\n readonly failingCheck: string | null;\n\n constructor(pullNumber: number, failingCheck: string | null) {\n super(\n `Pull request #${pullNumber} has failing checks${failingCheck ? ` (${failingCheck})` : \"\"}`,\n );\n this.name = \"ChecksFailedError\";\n this.pullNumber = pullNumber;\n this.failingCheck = failingCheck;\n }\n}\n","/**\n * The one place that talks to api.github.com.\n *\n * ── Every call is bounded ────────────────────────────────────────────────────\n * Same pattern as 5degrees-website's HighLevel client: a module constant plus\n * AbortSignal.timeout on every fetch. That fix exists because a slow upstream\n * ran past the serverless function limit and the process was killed before its\n * own error handler could run. A publish is ~10 sequential GitHub calls, so the\n * same failure is available here. Observed probe latency was 200–1400 ms; 10 s\n * is generous, and if GitHub is slower than that we want the error, not the wait.\n *\n * ── No module state ──────────────────────────────────────────────────────────\n * The token arrives in RepoContext on every call. Nothing is read from\n * process.env here or anywhere in src/git (BUILD-ENV law).\n */\n\nimport type { RepoContext } from \"./types\";\nimport { GitHubError, TokenScopeError } from \"./errors\";\n\nconst API = \"https://api.github.com\";\nexport const GITHUB_TIMEOUT_MS = 10_000;\n\nexport interface GitHubResponse<T = unknown> {\n ok: boolean;\n status: number;\n json: T;\n}\n\n/**\n * Low-level request. Returns instead of throwing on non-2xx so callers can\n * inspect the status when a \"failure\" is a meaningful answer (405 on merge,\n * 422 on a duplicate ref). Use `ghFetchOrThrow` when it is not.\n */\nexport async function ghFetch<T = unknown>(\n ctx: RepoContext,\n method: \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\",\n path: string,\n body?: unknown,\n): Promise<GitHubResponse<T>> {\n const res = await fetch(API + path, {\n method,\n signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),\n headers: {\n Authorization: `Bearer ${ctx.token}`,\n Accept: \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n \"User-Agent\": \"realiizlabs-admin\",\n ...(body !== undefined ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const text = await res.text();\n let json: unknown = null;\n if (text) {\n try {\n json = JSON.parse(text);\n } catch {\n json = { raw: text };\n }\n }\n return { ok: res.ok, status: res.status, json: json as T };\n}\n\n/** Same as ghFetch but a non-2xx becomes a typed error. */\nexport async function ghFetchOrThrow<T = unknown>(\n ctx: RepoContext,\n method: \"GET\" | \"POST\" | \"PATCH\" | \"PUT\" | \"DELETE\",\n path: string,\n body?: unknown,\n): Promise<T> {\n const res = await ghFetch<T>(ctx, method, path, body);\n if (!res.ok) throw toError(res, path);\n return res.json;\n}\n\nexport function toError(res: GitHubResponse, path: string): GitHubError {\n const opts = { status: res.status, path, body: res.json };\n if (res.status === 403) return new TokenScopeError(opts);\n const message =\n (res.json as { message?: string } | null)?.message ?? `GitHub ${res.status} on ${path}`;\n return new GitHubError(message, opts);\n}\n\n/** `/repos/{owner}/{repo}` prefix, built per call. */\nexport function repoPath(ctx: RepoContext): string {\n return `/repos/${encodeURIComponent(ctx.owner)}/${encodeURIComponent(ctx.repo)}`;\n}\n\nexport function baseBranch(ctx: RepoContext): string {\n return ctx.baseBranch ?? \"main\";\n}\n","/**\n * createBranch — a new branch off the base branch, as it is RIGHT NOW.\n *\n * The base sha is fetched from the refs API on every call and never stored.\n * This module is one of four writers to the same repo (github.com, GitHub\n * Desktop, Cowork are the others), so a ref that was current a minute ago is\n * routinely stale. Branching from a cached sha would silently build on old\n * content.\n */\n\nimport type { CreateBranchResult, RepoContext } from \"./types\";\nimport { baseBranch, ghFetchOrThrow, repoPath } from \"./client\";\n\ninterface RefResponse {\n ref: string;\n object: { sha: string; type: string };\n}\n\n/** Read the current tip of a branch. Always a live call. */\nexport async function getBranchSha(ctx: RepoContext, branch: string): Promise<string> {\n const ref = await ghFetchOrThrow<RefResponse>(\n ctx,\n \"GET\",\n `${repoPath(ctx)}/git/ref/heads/${encodeURIComponent(branch)}`,\n );\n return ref.object.sha;\n}\n\nexport async function createBranch(\n ctx: RepoContext,\n branch: string,\n): Promise<CreateBranchResult> {\n const baseSha = await getBranchSha(ctx, baseBranch(ctx));\n\n await ghFetchOrThrow<RefResponse>(ctx, \"POST\", `${repoPath(ctx)}/git/refs`, {\n ref: `refs/heads/${branch}`,\n sha: baseSha,\n });\n\n return { branch, baseSha };\n}\n\n/**\n * Remove a branch. 204 on success. Used after a squash merge so the client's\n * repo does not accumulate one dead branch per publish.\n */\nexport async function deleteBranch(ctx: RepoContext, branch: string): Promise<void> {\n await ghFetchOrThrow(\n ctx,\n \"DELETE\",\n `${repoPath(ctx)}/git/refs/heads/${encodeURIComponent(branch)}`,\n );\n}\n","/**\n * writeFiles — one or more files in ONE commit on an existing branch.\n *\n * Why the trees API and not the Contents API: a post is an MDX file plus a\n * hero image. Two Contents calls make two commits, and the first triggers a\n * build of a half-finished state. One tree, one commit.\n *\n * Encoding rule, observed in the probe: a tree entry can inline utf-8 text as\n * `content`, but has no `encoding` field — a binary must be uploaded first via\n * POST /git/blobs with `encoding: \"base64\"` and referenced in the tree by sha.\n * So a two-file publish with one image is six calls, not five.\n */\n\nimport type { FileInput, RepoContext, WriteFilesResult } from \"./types\";\nimport { getBranchSha } from \"./branch\";\nimport { ghFetchOrThrow, repoPath } from \"./client\";\n\ntype TreeEntry =\n | { path: string; mode: \"100644\"; type: \"blob\"; content: string }\n | { path: string; mode: \"100644\"; type: \"blob\"; sha: string };\n\nexport async function writeFiles(\n ctx: RepoContext,\n branch: string,\n files: FileInput[],\n message: string,\n): Promise<WriteFilesResult> {\n if (files.length === 0) throw new Error(\"writeFiles: no files given\");\n\n const base = repoPath(ctx);\n\n // The branch tip as of now — not the sha createBranch returned earlier.\n const parentSha = await getBranchSha(ctx, branch);\n\n // Binary files become blobs first; text is inlined.\n const tree: TreeEntry[] = [];\n for (const file of files) {\n if (file.encoding === \"base64\") {\n const blob = await ghFetchOrThrow<{ sha: string }>(ctx, \"POST\", `${base}/git/blobs`, {\n content: file.content,\n encoding: \"base64\",\n });\n tree.push({ path: file.path, mode: \"100644\", type: \"blob\", sha: blob.sha });\n } else {\n tree.push({ path: file.path, mode: \"100644\", type: \"blob\", content: file.content });\n }\n }\n\n const newTree = await ghFetchOrThrow<{ sha: string }>(ctx, \"POST\", `${base}/git/trees`, {\n base_tree: parentSha,\n tree,\n });\n\n const commit = await ghFetchOrThrow<{ sha: string }>(ctx, \"POST\", `${base}/git/commits`, {\n message,\n tree: newTree.sha,\n parents: [parentSha],\n });\n\n // Fast-forward the branch onto the new commit. `force: false` means a\n // concurrent write to this same branch surfaces as a 422 \"Update is not a\n // fast forward\" rather than being silently overwritten.\n await ghFetchOrThrow(\n ctx,\n \"PATCH\",\n `${base}/git/refs/heads/${encodeURIComponent(branch)}`,\n { sha: commit.sha, force: false },\n );\n\n return { branch, commitSha: commit.sha, treeSha: newTree.sha, fileCount: files.length };\n}\n","/**\n * openPullRequest — the PR that CI runs on and the client eventually publishes.\n *\n * PR Law (site-starter AGENTS.md): nothing lands on main except through a PR.\n * This module never pushes to the base branch directly.\n */\n\nimport type { PullRequest, RepoContext } from \"./types\";\nimport { baseBranch, ghFetchOrThrow, repoPath } from \"./client\";\n\n/** The subset of GitHub's PR payload this module reads. Observed in the probe. */\nexport interface PullPayload {\n number: number;\n html_url: string;\n state: \"open\" | \"closed\";\n head: { sha: string; ref: string };\n base: { sha: string; ref: string };\n /** null while GitHub is still computing — usually the first ~2 s after creation. */\n mergeable: boolean | null;\n mergeable_state: \"unknown\" | \"clean\" | \"dirty\" | \"blocked\" | \"unstable\" | \"behind\" | \"draft\" | \"has_hooks\";\n merged: boolean;\n}\n\nexport async function openPullRequest(\n ctx: RepoContext,\n branch: string,\n opts: { title: string; body?: string },\n): Promise<PullRequest> {\n const pr = await ghFetchOrThrow<PullPayload>(ctx, \"POST\", `${repoPath(ctx)}/pulls`, {\n title: opts.title,\n head: branch,\n base: baseBranch(ctx),\n body: opts.body ?? \"\",\n });\n\n return { number: pr.number, html_url: pr.html_url, headSha: pr.head.sha, branch };\n}\n\n/** Live read of a PR — the source of truth for mergeability. */\nexport async function getPullRequest(ctx: RepoContext, number: number): Promise<PullPayload> {\n return ghFetchOrThrow<PullPayload>(ctx, \"GET\", `${repoPath(ctx)}/pulls/${number}`);\n}\n","/**\n * getPullRequestStatus — green, red or pending. Nothing else.\n *\n * ── Why not the check-runs endpoint ──────────────────────────────────────────\n * A fine-grained PAT cannot read it. Observed 403 on every probe run, and the\n * token permission picker has no \"Checks\" entry to grant. So status is derived\n * from two things a PAT can always read:\n *\n * 1. the PR's `mergeable_state`, which folds in required checks when branch\n * protection is on;\n * 2. the combined commit status for the head sha, which is where Vercel and\n * any status-based CI report — and the only place a failing check has a\n * name.\n *\n * The accepted limitation: a GitHub Actions failure surfaces only as\n * `blocked`/`unstable`, not by job name. That is red without a name.\n *\n * ── Two traps, both observed ─────────────────────────────────────────────────\n * `mergeable` is null and `mergeable_state` is \"unknown\" for ~2 s after a PR is\n * created. That is pending, never red.\n *\n * The combined status of ZERO statuses has `state: \"pending\"`, not \"success\".\n * A no-CI repo read naively would be pending forever. So `total_count` is\n * checked before `state`.\n */\n\nimport type { PullRequestStatus, RepoContext } from \"./types\";\nimport { ghFetchOrThrow, repoPath } from \"./client\";\nimport { getPullRequest } from \"./pulls\";\n\n/** Observed shape of GET /commits/{sha}/status. */\nexport interface CombinedStatus {\n state: \"success\" | \"failure\" | \"pending\" | \"error\";\n total_count: number;\n statuses: Array<{ context: string; state: string; description: string | null }>;\n}\n\nexport async function getPullRequestStatus(\n ctx: RepoContext,\n number: number,\n log: Pick<Console, \"warn\"> = console,\n): Promise<PullRequestStatus> {\n const pr = await getPullRequest(ctx, number);\n\n if (pr.mergeable === null || pr.mergeable_state === \"unknown\") {\n return { state: \"pending\" };\n }\n\n const combined = await ghFetchOrThrow<CombinedStatus>(\n ctx,\n \"GET\",\n `${repoPath(ctx)}/commits/${pr.head.sha}/status`,\n );\n\n const failing = combined.statuses.find((s) => s.state === \"failure\" || s.state === \"error\");\n\n // dirty is a conflict, not a check failure — mergePullRequest turns it into\n // StaleBaseError. Report it as red here so a poller stops polling.\n if (pr.mergeable_state === \"dirty\") {\n return { state: \"red\", failingCheck: failing?.context ?? \"merge conflict\" };\n }\n\n if (failing || pr.mergeable_state === \"unstable\") {\n return { state: \"red\", failingCheck: failing?.context ?? null };\n }\n\n if (combined.total_count === 0) {\n if (pr.mergeable_state === \"clean\" || pr.mergeable_state === \"behind\") {\n const warning = `[admin/git] ${ctx.owner}/${ctx.repo} has no CI statuses on PR #${number} — treating as green. A client site with no CI is a setup bug.`;\n log.warn(warning);\n return { state: \"green\", warning };\n }\n // blocked with nothing reported yet: required checks have not started.\n return { state: \"pending\" };\n }\n\n if (combined.state === \"success\" && pr.mergeable_state !== \"blocked\") {\n return { state: \"green\" };\n }\n\n return { state: \"pending\" };\n}\n","/**\n * mergePullRequest — publish. Squash, then delete the branch.\n *\n * `requireGreen` defaults to true. The whole point of the PR flow is that CI\n * catches bad frontmatter before it freezes the site; merging red automates the\n * exact failure the flow exists to prevent.\n *\n * Pending is the common case (checks are always pending right after a push),\n * so it is returned for the caller to poll — never thrown.\n */\n\nimport type { MergeResult, RepoContext } from \"./types\";\nimport { deleteBranch } from \"./branch\";\nimport { ghFetch, repoPath, toError } from \"./client\";\nimport { ChecksFailedError, StaleBaseError } from \"./errors\";\nimport { getPullRequest } from \"./pulls\";\nimport { getPullRequestStatus } from \"./status\";\n\ninterface MergeResponse {\n sha: string;\n merged: boolean;\n message: string;\n}\n\nexport interface MergeOptions {\n /** Refuse to merge unless checks are green. Default true. */\n requireGreen?: boolean;\n /** Delete the head branch after a successful merge. Default true. */\n deleteBranch?: boolean;\n /** Squash commit title. Defaults to GitHub's (the PR title + number). */\n commitTitle?: string;\n}\n\nexport async function mergePullRequest(\n ctx: RepoContext,\n number: number,\n opts: MergeOptions = {},\n): Promise<MergeResult> {\n const requireGreen = opts.requireGreen ?? true;\n const path = `${repoPath(ctx)}/pulls/${number}/merge`;\n\n if (requireGreen) {\n const status = await getPullRequestStatus(ctx, number);\n if (status.state === \"pending\") return { merged: false, state: \"pending\" };\n if (status.state === \"red\") {\n // A conflict is not a failed check — it is a stale base. Distinguish it.\n const pr = await getPullRequest(ctx, number);\n if (pr.mergeable_state === \"dirty\") {\n throw new StaleBaseError(number, { status: 0, path, body: pr });\n }\n throw new ChecksFailedError(number, status.failingCheck);\n }\n }\n\n const res = await ghFetch<MergeResponse>(ctx, \"PUT\", path, {\n merge_method: \"squash\",\n ...(opts.commitTitle ? { commit_title: opts.commitTitle } : {}),\n });\n\n if (!res.ok) {\n // Observed: 405 { message: \"Pull Request has merge conflicts\" } for a\n // stale base. Also 405 for \"not mergeable\" generally, and 409 if the head\n // sha moved between read and merge — both are the same recovery for the\n // caller (re-read, rebuild), so both map to StaleBaseError.\n if (res.status === 405 || res.status === 409) {\n throw new StaleBaseError(number, { status: res.status, path, body: res.json });\n }\n throw toError(res, path);\n }\n\n if (opts.deleteBranch ?? true) {\n const pr = await getPullRequest(ctx, number);\n await deleteBranch(ctx, pr.head.ref);\n }\n\n return { merged: true, sha: res.json.sha };\n}\n","/**\n * Read helpers — list a folder, open a file — straight from the repo at call time.\n *\n * The dashboard lists and opens content from `main` via the Contents API, never\n * from the deployed filesystem: Vercel's bundle only contains what the tracer\n * saw, and it is a snapshot of the last deploy rather than of the repo. This\n * is also how hand-written files (github.com, Desktop, Cowork) show up — the\n * dashboard never assumes it authored anything.\n */\n\nimport type { RepoContext } from \"./types\";\nimport { baseBranch, ghFetch, repoPath, toError } from \"./client\";\nimport { GitHubError } from \"./errors\";\n\nexport interface RepoEntry {\n name: string;\n path: string;\n sha: string;\n size: number;\n type: \"file\";\n}\n\nexport interface RepoFile {\n path: string;\n sha: string;\n content: string;\n}\n\nexport class NotFoundError extends GitHubError {\n constructor(path: string) {\n super(`Not found in repo: ${path}`, { status: 404, path, body: null });\n this.name = \"NotFoundError\";\n }\n}\n\ninterface ContentsEntry {\n name: string;\n path: string;\n sha: string;\n size: number;\n type: \"file\" | \"dir\" | \"symlink\" | \"submodule\";\n content?: string;\n encoding?: string;\n}\n\nfunction contentsPath(ctx: RepoContext, path: string, ref?: string): string {\n const clean = path.replace(/^\\/+|\\/+$/g, \"\");\n const encoded = clean.split(\"/\").map(encodeURIComponent).join(\"/\");\n return `${repoPath(ctx)}/contents/${encoded}?ref=${encodeURIComponent(ref ?? baseBranch(ctx))}`;\n}\n\n/** Files directly inside `path`, sorted by name. Missing folder → []. */\nexport async function listDirectory(ctx: RepoContext, path: string, opts: { ref?: string } = {}): Promise<RepoEntry[]> {\n const url = contentsPath(ctx, path, opts.ref);\n const res = await ghFetch<ContentsEntry[] | ContentsEntry>(ctx, \"GET\", url);\n if (res.status === 404) return [];\n if (!res.ok) throw toError(res, url);\n const list = Array.isArray(res.json) ? res.json : [];\n return list\n .filter((e) => e.type === \"file\")\n .map((e) => ({ name: e.name, path: e.path, sha: e.sha, size: e.size, type: \"file\" as const }))\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/** One file, decoded to utf-8. */\nexport async function readFile(ctx: RepoContext, path: string, opts: { ref?: string } = {}): Promise<RepoFile> {\n const url = contentsPath(ctx, path, opts.ref);\n const res = await ghFetch<ContentsEntry>(ctx, \"GET\", url);\n if (res.status === 404) throw new NotFoundError(path);\n if (!res.ok) throw toError(res, url);\n const entry = res.json;\n if (Array.isArray(entry) || entry.type !== \"file\") throw new GitHubError(`${path} is not a file`, { status: res.status, path: url, body: entry });\n if (typeof entry.content !== \"string\") {\n // GitHub omits content for files over 1 MB; MDX never gets near that.\n throw new GitHubError(`${path} is too large to read through the Contents API`, { status: res.status, path: url, body: null });\n }\n const content = decodeBase64Utf8(entry.content);\n return { path: entry.path, sha: entry.sha, content };\n}\n\n/** The Contents API wraps base64 at 60 columns; strip whitespace before decoding. */\nfunction decodeBase64Utf8(b64: string): string {\n const clean = b64.replace(/\\s+/g, \"\");\n const bin = atob(clean);\n const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));\n return new TextDecoder(\"utf-8\").decode(bytes);\n}\n"]}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the git write layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is an argument type or a return type. There is no module
|
|
5
|
+
* state anywhere in src/git — every function receives a RepoContext so the
|
|
6
|
+
* token is never read from process.env at import time (BUILD-ENV law) and the
|
|
7
|
+
* caller decides which site's repo it is talking to.
|
|
8
|
+
*/
|
|
9
|
+
/** Which repo, and with what credential. Passed to every function. */
|
|
10
|
+
interface RepoContext {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
/**
|
|
14
|
+
* Fine-grained PAT scoped to this one repo. Required permissions (observed in
|
|
15
|
+
* the ADMIN-01 probe): Contents RW, Pull requests RW, Commit statuses R.
|
|
16
|
+
* Lives in the site's server env; never reaches a browser.
|
|
17
|
+
*/
|
|
18
|
+
token: string;
|
|
19
|
+
/** Default branch to branch from and merge into. Defaults to "main". */
|
|
20
|
+
baseBranch?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A utf-8 text file, e.g. an MDX document. Inlined into the tree. */
|
|
23
|
+
interface TextFile {
|
|
24
|
+
path: string;
|
|
25
|
+
content: string;
|
|
26
|
+
encoding?: "utf-8";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A binary file, e.g. an image. Must be uploaded as a blob first — the trees
|
|
30
|
+
* API cannot inline base64 (observed: no `encoding` field on tree entries).
|
|
31
|
+
*/
|
|
32
|
+
interface BinaryFile {
|
|
33
|
+
path: string;
|
|
34
|
+
content: string;
|
|
35
|
+
encoding: "base64";
|
|
36
|
+
}
|
|
37
|
+
type FileInput = TextFile | BinaryFile;
|
|
38
|
+
interface CreateBranchResult {
|
|
39
|
+
branch: string;
|
|
40
|
+
/** The sha of the base branch at the moment the branch was created. */
|
|
41
|
+
baseSha: string;
|
|
42
|
+
}
|
|
43
|
+
interface WriteFilesResult {
|
|
44
|
+
branch: string;
|
|
45
|
+
commitSha: string;
|
|
46
|
+
treeSha: string;
|
|
47
|
+
/** How many files landed in the single commit. */
|
|
48
|
+
fileCount: number;
|
|
49
|
+
}
|
|
50
|
+
interface PullRequest {
|
|
51
|
+
number: number;
|
|
52
|
+
html_url: string;
|
|
53
|
+
headSha: string;
|
|
54
|
+
branch: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Exactly three states. `pending` is the normal condition right after a push
|
|
58
|
+
* and while GitHub is still computing mergeability — never an error.
|
|
59
|
+
*/
|
|
60
|
+
type PullRequestStatus = {
|
|
61
|
+
state: "green";
|
|
62
|
+
warning?: string;
|
|
63
|
+
} | {
|
|
64
|
+
state: "red";
|
|
65
|
+
failingCheck: string | null;
|
|
66
|
+
} | {
|
|
67
|
+
state: "pending";
|
|
68
|
+
};
|
|
69
|
+
type MergeResult = {
|
|
70
|
+
merged: true;
|
|
71
|
+
sha: string;
|
|
72
|
+
} | {
|
|
73
|
+
merged: false;
|
|
74
|
+
state: "pending";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* createBranch — a new branch off the base branch, as it is RIGHT NOW.
|
|
79
|
+
*
|
|
80
|
+
* The base sha is fetched from the refs API on every call and never stored.
|
|
81
|
+
* This module is one of four writers to the same repo (github.com, GitHub
|
|
82
|
+
* Desktop, Cowork are the others), so a ref that was current a minute ago is
|
|
83
|
+
* routinely stale. Branching from a cached sha would silently build on old
|
|
84
|
+
* content.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
declare function createBranch(ctx: RepoContext, branch: string): Promise<CreateBranchResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Remove a branch. 204 on success. Used after a squash merge so the client's
|
|
90
|
+
* repo does not accumulate one dead branch per publish.
|
|
91
|
+
*/
|
|
92
|
+
declare function deleteBranch(ctx: RepoContext, branch: string): Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* writeFiles — one or more files in ONE commit on an existing branch.
|
|
96
|
+
*
|
|
97
|
+
* Why the trees API and not the Contents API: a post is an MDX file plus a
|
|
98
|
+
* hero image. Two Contents calls make two commits, and the first triggers a
|
|
99
|
+
* build of a half-finished state. One tree, one commit.
|
|
100
|
+
*
|
|
101
|
+
* Encoding rule, observed in the probe: a tree entry can inline utf-8 text as
|
|
102
|
+
* `content`, but has no `encoding` field — a binary must be uploaded first via
|
|
103
|
+
* POST /git/blobs with `encoding: "base64"` and referenced in the tree by sha.
|
|
104
|
+
* So a two-file publish with one image is six calls, not five.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
declare function writeFiles(ctx: RepoContext, branch: string, files: FileInput[], message: string): Promise<WriteFilesResult>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* openPullRequest — the PR that CI runs on and the client eventually publishes.
|
|
111
|
+
*
|
|
112
|
+
* PR Law (site-starter AGENTS.md): nothing lands on main except through a PR.
|
|
113
|
+
* This module never pushes to the base branch directly.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
declare function openPullRequest(ctx: RepoContext, branch: string, opts: {
|
|
117
|
+
title: string;
|
|
118
|
+
body?: string;
|
|
119
|
+
}): Promise<PullRequest>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* getPullRequestStatus — green, red or pending. Nothing else.
|
|
123
|
+
*
|
|
124
|
+
* ── Why not the check-runs endpoint ──────────────────────────────────────────
|
|
125
|
+
* A fine-grained PAT cannot read it. Observed 403 on every probe run, and the
|
|
126
|
+
* token permission picker has no "Checks" entry to grant. So status is derived
|
|
127
|
+
* from two things a PAT can always read:
|
|
128
|
+
*
|
|
129
|
+
* 1. the PR's `mergeable_state`, which folds in required checks when branch
|
|
130
|
+
* protection is on;
|
|
131
|
+
* 2. the combined commit status for the head sha, which is where Vercel and
|
|
132
|
+
* any status-based CI report — and the only place a failing check has a
|
|
133
|
+
* name.
|
|
134
|
+
*
|
|
135
|
+
* The accepted limitation: a GitHub Actions failure surfaces only as
|
|
136
|
+
* `blocked`/`unstable`, not by job name. That is red without a name.
|
|
137
|
+
*
|
|
138
|
+
* ── Two traps, both observed ─────────────────────────────────────────────────
|
|
139
|
+
* `mergeable` is null and `mergeable_state` is "unknown" for ~2 s after a PR is
|
|
140
|
+
* created. That is pending, never red.
|
|
141
|
+
*
|
|
142
|
+
* The combined status of ZERO statuses has `state: "pending"`, not "success".
|
|
143
|
+
* A no-CI repo read naively would be pending forever. So `total_count` is
|
|
144
|
+
* checked before `state`.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
declare function getPullRequestStatus(ctx: RepoContext, number: number, log?: Pick<Console, "warn">): Promise<PullRequestStatus>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* mergePullRequest — publish. Squash, then delete the branch.
|
|
151
|
+
*
|
|
152
|
+
* `requireGreen` defaults to true. The whole point of the PR flow is that CI
|
|
153
|
+
* catches bad frontmatter before it freezes the site; merging red automates the
|
|
154
|
+
* exact failure the flow exists to prevent.
|
|
155
|
+
*
|
|
156
|
+
* Pending is the common case (checks are always pending right after a push),
|
|
157
|
+
* so it is returned for the caller to poll — never thrown.
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
interface MergeOptions {
|
|
161
|
+
/** Refuse to merge unless checks are green. Default true. */
|
|
162
|
+
requireGreen?: boolean;
|
|
163
|
+
/** Delete the head branch after a successful merge. Default true. */
|
|
164
|
+
deleteBranch?: boolean;
|
|
165
|
+
/** Squash commit title. Defaults to GitHub's (the PR title + number). */
|
|
166
|
+
commitTitle?: string;
|
|
167
|
+
}
|
|
168
|
+
declare function mergePullRequest(ctx: RepoContext, number: number, opts?: MergeOptions): Promise<MergeResult>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Typed errors — so ADMIN-04 can `instanceof` rather than string-match.
|
|
172
|
+
*
|
|
173
|
+
* Each carries enough to log usefully without ever including the token.
|
|
174
|
+
*/
|
|
175
|
+
/** Any non-2xx from GitHub that is not one of the more specific cases below. */
|
|
176
|
+
declare class GitHubError extends Error {
|
|
177
|
+
readonly status: number;
|
|
178
|
+
readonly path: string;
|
|
179
|
+
readonly body: unknown;
|
|
180
|
+
constructor(message: string, opts: {
|
|
181
|
+
status: number;
|
|
182
|
+
path: string;
|
|
183
|
+
body: unknown;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The pull request cannot merge because `main` moved underneath it in a way
|
|
188
|
+
* that conflicts — someone else edited the same file first.
|
|
189
|
+
*
|
|
190
|
+
* Observed in the probe: a stale base sha does NOT fail the trees write. Tree,
|
|
191
|
+
* commit, ref and PR all succeed; the conflict only shows as
|
|
192
|
+
* `mergeable_state: "dirty"` and then a 405 "Pull Request has merge conflicts"
|
|
193
|
+
* on merge. So this is raised from mergePullRequest, never from writeFiles.
|
|
194
|
+
* The recovery is the caller's: re-read main, rebuild the change, open a new PR.
|
|
195
|
+
*/
|
|
196
|
+
declare class StaleBaseError extends GitHubError {
|
|
197
|
+
readonly pullNumber: number;
|
|
198
|
+
constructor(pullNumber: number, opts: {
|
|
199
|
+
status: number;
|
|
200
|
+
path: string;
|
|
201
|
+
body: unknown;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The token cannot do what was asked. A 403 from GitHub with a fine-grained PAT
|
|
206
|
+
* almost always means a permission was not granted when the token was made —
|
|
207
|
+
* a setup bug at onboarding, not a runtime condition.
|
|
208
|
+
*/
|
|
209
|
+
declare class TokenScopeError extends GitHubError {
|
|
210
|
+
static readonly REQUIRED_PERMISSIONS = "Contents: Read and write, Pull requests: Read and write, Commit statuses: Read-only";
|
|
211
|
+
constructor(opts: {
|
|
212
|
+
status: number;
|
|
213
|
+
path: string;
|
|
214
|
+
body: unknown;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** mergePullRequest was asked to merge while checks are red. */
|
|
218
|
+
declare class ChecksFailedError extends Error {
|
|
219
|
+
readonly pullNumber: number;
|
|
220
|
+
readonly failingCheck: string | null;
|
|
221
|
+
constructor(pullNumber: number, failingCheck: string | null);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Read helpers — list a folder, open a file — straight from the repo at call time.
|
|
226
|
+
*
|
|
227
|
+
* The dashboard lists and opens content from `main` via the Contents API, never
|
|
228
|
+
* from the deployed filesystem: Vercel's bundle only contains what the tracer
|
|
229
|
+
* saw, and it is a snapshot of the last deploy rather than of the repo. This
|
|
230
|
+
* is also how hand-written files (github.com, Desktop, Cowork) show up — the
|
|
231
|
+
* dashboard never assumes it authored anything.
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
interface RepoEntry {
|
|
235
|
+
name: string;
|
|
236
|
+
path: string;
|
|
237
|
+
sha: string;
|
|
238
|
+
size: number;
|
|
239
|
+
type: "file";
|
|
240
|
+
}
|
|
241
|
+
interface RepoFile {
|
|
242
|
+
path: string;
|
|
243
|
+
sha: string;
|
|
244
|
+
content: string;
|
|
245
|
+
}
|
|
246
|
+
declare class NotFoundError extends GitHubError {
|
|
247
|
+
constructor(path: string);
|
|
248
|
+
}
|
|
249
|
+
/** Files directly inside `path`, sorted by name. Missing folder → []. */
|
|
250
|
+
declare function listDirectory(ctx: RepoContext, path: string, opts?: {
|
|
251
|
+
ref?: string;
|
|
252
|
+
}): Promise<RepoEntry[]>;
|
|
253
|
+
/** One file, decoded to utf-8. */
|
|
254
|
+
declare function readFile(ctx: RepoContext, path: string, opts?: {
|
|
255
|
+
ref?: string;
|
|
256
|
+
}): Promise<RepoFile>;
|
|
257
|
+
|
|
258
|
+
export { type BinaryFile, ChecksFailedError, type CreateBranchResult, type FileInput, GitHubError, type MergeOptions, type MergeResult, NotFoundError, type PullRequest, type PullRequestStatus, type RepoContext, type RepoEntry, type RepoFile, StaleBaseError, type TextFile, TokenScopeError, type WriteFilesResult, createBranch, deleteBranch, getPullRequestStatus, listDirectory, mergePullRequest, openPullRequest, readFile, writeFiles };
|