@stacksjs/github 0.70.162 → 0.70.163
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/client.d.ts +9 -0
- package/dist/client.js +49 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2 -1
- package/dist/pull-requests.d.ts +43 -0
- package/dist/pull-requests.js +83 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare function ghHeaders(): Record<string, string>;
|
|
2
|
+
export declare function githubHeaders(token?: string): Record<string, string>;
|
|
3
|
+
/** Provider-neutral GitHub request helper for both reads and writes. */
|
|
4
|
+
export declare function githubRequest(path: string, init?: RequestInit, options?: GitHubClientOptions, attempt?: number): Promise<Response>;
|
|
5
|
+
export declare function githubJson<T>(path: string, init?: RequestInit, options?: GitHubClientOptions): Promise<T>;
|
|
2
6
|
/**
|
|
3
7
|
* `fetch` against the GitHub API that retries on secondary rate limits.
|
|
4
8
|
*
|
|
@@ -13,3 +17,8 @@ export declare function ghFetch(url: string, attempt?: number): Promise<Response
|
|
|
13
17
|
*/
|
|
14
18
|
export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
|
|
15
19
|
export declare const GITHUB_API: 'https://api.github.com';
|
|
20
|
+
export declare interface GitHubClientOptions {
|
|
21
|
+
token?: string
|
|
22
|
+
apiUrl?: string
|
|
23
|
+
fetch?: typeof globalThis.fetch
|
|
24
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -5,6 +5,11 @@ function getToken() {
|
|
|
5
5
|
throw Error("GITHUB_TOKEN environment variable is required");
|
|
6
6
|
return token;
|
|
7
7
|
}
|
|
8
|
+
function resolveToken(token) {
|
|
9
|
+
if (token)
|
|
10
|
+
return token;
|
|
11
|
+
return getToken();
|
|
12
|
+
}
|
|
8
13
|
export function ghHeaders() {
|
|
9
14
|
return {
|
|
10
15
|
Authorization: `Bearer ${getToken()}`,
|
|
@@ -12,6 +17,50 @@ export function ghHeaders() {
|
|
|
12
17
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
13
18
|
};
|
|
14
19
|
}
|
|
20
|
+
export function githubHeaders(token) {
|
|
21
|
+
return {
|
|
22
|
+
Authorization: `Bearer ${resolveToken(token)}`,
|
|
23
|
+
Accept: "application/vnd.github+json",
|
|
24
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function retryDelay(res, attempt) {
|
|
28
|
+
if (!(res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || !!res.headers.get("retry-after"))))
|
|
29
|
+
return null;
|
|
30
|
+
const retryAfterHeader = res.headers.get("retry-after"), resetHeader = res.headers.get("x-ratelimit-reset");
|
|
31
|
+
if (retryAfterHeader)
|
|
32
|
+
return Number(retryAfterHeader) * 1000;
|
|
33
|
+
if (resetHeader)
|
|
34
|
+
return Math.max(0, Number(resetHeader) * 1000 - Date.now()) + 500;
|
|
35
|
+
return 1000 * 2 ** attempt;
|
|
36
|
+
}
|
|
37
|
+
export async function githubRequest(path, init = {}, options = {}, attempt = 0) {
|
|
38
|
+
const fetcher = options.fetch ?? globalThis.fetch, url = path.startsWith("http") ? path : `${options.apiUrl ?? GITHUB_API}${path.startsWith("/") ? path : `/${path}`}`, response = await fetcher(url, {
|
|
39
|
+
...init,
|
|
40
|
+
headers: { ...githubHeaders(options.token), ...init.headers }
|
|
41
|
+
});
|
|
42
|
+
if (response.ok || attempt >= 3)
|
|
43
|
+
return response;
|
|
44
|
+
const waitMs = retryDelay(response, attempt);
|
|
45
|
+
if (waitMs === null)
|
|
46
|
+
return response;
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(waitMs, 30000)));
|
|
48
|
+
return githubRequest(path, init, options, attempt + 1);
|
|
49
|
+
}
|
|
50
|
+
export async function githubJson(path, init = {}, options = {}) {
|
|
51
|
+
const response = await githubRequest(path, init, options);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const payload = await response.text();
|
|
54
|
+
let message = payload;
|
|
55
|
+
try {
|
|
56
|
+
message = JSON.parse(payload).message || payload;
|
|
57
|
+
} catch {}
|
|
58
|
+
throw Error(`GitHub API ${response.status}: ${message || response.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
if (response.status === 204)
|
|
61
|
+
return;
|
|
62
|
+
return await response.json();
|
|
63
|
+
}
|
|
15
64
|
export async function ghFetch(url, attempt = 0) {
|
|
16
65
|
const res = await fetch(url, { headers: ghHeaders() });
|
|
17
66
|
if (res.ok || attempt >= 3)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type { GitHubClientOptions } from './client';
|
|
1
2
|
export type { DetectOptions, FailedTransition, PreviousRunState } from './failure-detector';
|
|
2
3
|
export type { WorkflowJob, WorkflowRun } from './run-history';
|
|
3
4
|
export type {
|
|
@@ -6,6 +7,13 @@ export type {
|
|
|
6
7
|
RunnerAlertState,
|
|
7
8
|
RunnerSample,
|
|
8
9
|
} from './runner-pressure-detector';
|
|
10
|
+
export type {
|
|
11
|
+
CreatedPullRequest,
|
|
12
|
+
CreatePullRequestWithFilesOptions,
|
|
13
|
+
RepositoryFileChange,
|
|
14
|
+
RepositoryTree,
|
|
15
|
+
RepositoryTreeEntry,
|
|
16
|
+
} from './pull-requests';
|
|
9
17
|
export type {
|
|
10
18
|
DashboardData,
|
|
11
19
|
DashboardOptions,
|
|
@@ -25,7 +33,7 @@ export type {
|
|
|
25
33
|
* single dimension without pulling the whole snapshot through.
|
|
26
34
|
*/
|
|
27
35
|
export { fetchBotPRCounts } from './bots';
|
|
28
|
-
export { ghFetch, ghHeaders, GITHUB_API, mapWithConcurrency } from './client';
|
|
36
|
+
export { ghFetch, ghHeaders, githubHeaders, githubJson, githubRequest, GITHUB_API, mapWithConcurrency } from './client';
|
|
29
37
|
export { clearDashboardCache, getDashboardData } from './dashboard';
|
|
30
38
|
export { detectNewlyFailedRuns } from './failure-detector';
|
|
31
39
|
export { fetchAllRepos } from './repos';
|
|
@@ -33,3 +41,4 @@ export { fetchRepoActiveRuns } from './runners';
|
|
|
33
41
|
export { fetchRunJobs, fetchWorkflowRuns } from './run-history';
|
|
34
42
|
export { detectRunnerPressure } from './runner-pressure-detector';
|
|
35
43
|
export { fetchFailedJobs, fetchRepoStatus } from './runs';
|
|
44
|
+
export { createPullRequestWithFiles, fetchRepositoryFile, fetchRepositoryTree } from './pull-requests';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { fetchBotPRCounts } from "./bots";
|
|
2
|
-
export { ghFetch, ghHeaders, GITHUB_API, mapWithConcurrency } from "./client";
|
|
2
|
+
export { ghFetch, ghHeaders, githubHeaders, githubJson, githubRequest, GITHUB_API, mapWithConcurrency } from "./client";
|
|
3
3
|
export { clearDashboardCache, getDashboardData } from "./dashboard";
|
|
4
4
|
export { detectNewlyFailedRuns } from "./failure-detector";
|
|
5
5
|
export { fetchAllRepos } from "./repos";
|
|
@@ -7,3 +7,4 @@ export { fetchRepoActiveRuns } from "./runners";
|
|
|
7
7
|
export { fetchRunJobs, fetchWorkflowRuns } from "./run-history";
|
|
8
8
|
export { detectRunnerPressure } from "./runner-pressure-detector";
|
|
9
9
|
export { fetchFailedJobs, fetchRepoStatus } from "./runs";
|
|
10
|
+
export { createPullRequestWithFiles, fetchRepositoryFile, fetchRepositoryTree } from "./pull-requests";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GitHubClientOptions } from './client';
|
|
2
|
+
export declare function fetchRepositoryTree(owner: string, repo: string, ref: string, options?: GitHubClientOptions): Promise<RepositoryTree>;
|
|
3
|
+
export declare function fetchRepositoryFile(owner: string, repo: string, path: string, ref: string, options?: GitHubClientOptions & { maxBytes?: number }): Promise<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Create one commit containing all file replacements, expose it through a new
|
|
6
|
+
* branch, and open a pull request. The branch is only created after every blob,
|
|
7
|
+
* tree, and commit has succeeded, so partial generation never exposes half a fix.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createPullRequestWithFiles(options: CreatePullRequestWithFilesOptions): Promise<CreatedPullRequest>;
|
|
10
|
+
export declare interface RepositoryFileChange {
|
|
11
|
+
path: string
|
|
12
|
+
content: string
|
|
13
|
+
}
|
|
14
|
+
export declare interface CreatePullRequestWithFilesOptions extends GitHubClientOptions {
|
|
15
|
+
owner: string
|
|
16
|
+
repo: string
|
|
17
|
+
branch: string
|
|
18
|
+
title: string
|
|
19
|
+
body: string
|
|
20
|
+
commitMessage: string
|
|
21
|
+
files: RepositoryFileChange[]
|
|
22
|
+
base?: string
|
|
23
|
+
draft?: boolean
|
|
24
|
+
}
|
|
25
|
+
export declare interface CreatedPullRequest {
|
|
26
|
+
number: number
|
|
27
|
+
url: string
|
|
28
|
+
branch: string
|
|
29
|
+
base: string
|
|
30
|
+
commitSha: string
|
|
31
|
+
}
|
|
32
|
+
export declare interface RepositoryTreeEntry {
|
|
33
|
+
path: string
|
|
34
|
+
mode: string
|
|
35
|
+
type: 'blob' | 'tree' | 'commit'
|
|
36
|
+
sha: string
|
|
37
|
+
size?: number
|
|
38
|
+
}
|
|
39
|
+
export declare interface RepositoryTree {
|
|
40
|
+
sha: string
|
|
41
|
+
truncated: boolean
|
|
42
|
+
entries: RepositoryTreeEntry[]
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { githubJson } from "./client";
|
|
2
|
+
function repoPath(owner, repo) {
|
|
3
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo))
|
|
4
|
+
throw Error("GitHub owner and repository names contain invalid characters");
|
|
5
|
+
return `/repos/${owner}/${repo}`;
|
|
6
|
+
}
|
|
7
|
+
function validateBranch(branch) {
|
|
8
|
+
if (!branch || branch.length > 240 || branch.startsWith("/") || branch.endsWith("/") || branch.includes("..") || /[~^:?*[\\\s]/.test(branch))
|
|
9
|
+
throw Error(`Invalid Git branch name: ${branch}`);
|
|
10
|
+
}
|
|
11
|
+
function validateFiles(files) {
|
|
12
|
+
if (!files.length)
|
|
13
|
+
throw Error("At least one file change is required");
|
|
14
|
+
if (files.length > 100)
|
|
15
|
+
throw Error("A pull request may change at most 100 files");
|
|
16
|
+
const seen = new Set;
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").includes(".."))
|
|
20
|
+
throw Error(`Unsafe repository path: ${file.path}`);
|
|
21
|
+
if (seen.has(file.path))
|
|
22
|
+
throw Error(`Duplicate repository path: ${file.path}`);
|
|
23
|
+
seen.add(file.path);
|
|
24
|
+
bytes += Buffer.byteLength(file.content);
|
|
25
|
+
}
|
|
26
|
+
if (bytes > 5242880)
|
|
27
|
+
throw Error("Combined file content exceeds the 5 MiB safety limit");
|
|
28
|
+
}
|
|
29
|
+
export async function fetchRepositoryTree(owner, repo, ref, options = {}) {
|
|
30
|
+
const payload = await githubJson(`${repoPath(owner, repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`, {}, options);
|
|
31
|
+
return { sha: payload.sha, truncated: !!payload.truncated, entries: payload.tree ?? [] };
|
|
32
|
+
}
|
|
33
|
+
export async function fetchRepositoryFile(owner, repo, path, ref, options = {}) {
|
|
34
|
+
validateFiles([{ path, content: "" }]);
|
|
35
|
+
const payload = await githubJson(`${repoPath(owner, repo)}/contents/${path.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`, {}, options);
|
|
36
|
+
if (payload.type !== "file" || payload.encoding !== "base64" || typeof payload.content !== "string")
|
|
37
|
+
throw Error(`GitHub path is not a base64 encoded file: ${path}`);
|
|
38
|
+
const maxBytes = options.maxBytes ?? 262144;
|
|
39
|
+
if ((payload.size ?? 0) > maxBytes)
|
|
40
|
+
throw Error(`GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);
|
|
41
|
+
const content = Buffer.from(payload.content.replace(/\n/g, ""), "base64");
|
|
42
|
+
if (content.byteLength > maxBytes)
|
|
43
|
+
throw Error(`Decoded GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);
|
|
44
|
+
return content.toString("utf8");
|
|
45
|
+
}
|
|
46
|
+
export async function createPullRequestWithFiles(options) {
|
|
47
|
+
validateBranch(options.branch);
|
|
48
|
+
validateFiles(options.files);
|
|
49
|
+
const basePath = repoPath(options.owner, options.repo), client = { token: options.token, apiUrl: options.apiUrl, fetch: options.fetch };
|
|
50
|
+
let base = options.base;
|
|
51
|
+
if (!base)
|
|
52
|
+
base = (await githubJson(basePath, {}, client)).default_branch;
|
|
53
|
+
if (!base)
|
|
54
|
+
throw Error("GitHub repository has no default branch");
|
|
55
|
+
const reference = await githubJson(`${basePath}/git/ref/heads/${encodeURIComponent(base)}`, {}, client), baseCommit = await githubJson(`${basePath}/git/commits/${reference.object.sha}`, {}, client), blobs = await Promise.all(options.files.map(async (file) => {
|
|
56
|
+
const blob = await githubJson(`${basePath}/git/blobs`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: JSON.stringify({ content: file.content, encoding: "utf-8" })
|
|
59
|
+
}, client);
|
|
60
|
+
return { path: file.path, mode: "100644", type: "blob", sha: blob.sha };
|
|
61
|
+
})), tree = await githubJson(`${basePath}/git/trees`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({ base_tree: baseCommit.tree.sha, tree: blobs })
|
|
64
|
+
}, client), commit = await githubJson(`${basePath}/git/commits`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({ message: options.commitMessage, tree: tree.sha, parents: [reference.object.sha] })
|
|
67
|
+
}, client);
|
|
68
|
+
await githubJson(`${basePath}/git/refs`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ ref: `refs/heads/${options.branch}`, sha: commit.sha })
|
|
71
|
+
}, client);
|
|
72
|
+
const pull = await githubJson(`${basePath}/pulls`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
title: options.title,
|
|
76
|
+
body: options.body,
|
|
77
|
+
head: options.branch,
|
|
78
|
+
base,
|
|
79
|
+
draft: options.draft ?? !0
|
|
80
|
+
})
|
|
81
|
+
}, client);
|
|
82
|
+
return { number: pull.number, url: pull.html_url, branch: options.branch, base, commitSha: commit.sha };
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/github",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.163",
|
|
5
5
|
"description": "GitHub API client used by Stacks framework features (dashboard CI surface, notifications, runner alerts).",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|