Upload files to "src/mutator/branch"
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import type { RepoContext } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the owner/repo from standard GitHub Actions environment variables.
|
||||||
|
*
|
||||||
|
* GitHub Actions automatically sets:
|
||||||
|
* - GITHUB_REPOSITORY e.g. "octocat/hello-world"
|
||||||
|
* - GITHUB_REPOSITORY_OWNER e.g. "octocat"
|
||||||
|
*
|
||||||
|
* @see https://docs.github.com/en/actions/learn-github-actions/variables
|
||||||
|
*/
|
||||||
|
export function resolveRepoFromEnv(): RepoContext {
|
||||||
|
const repository = process.env["GITHUB_REPOSITORY"];
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
throw new Error(
|
||||||
|
"GITHUB_REPOSITORY env var is not set. This must be run inside a GitHub Actions workflow, " +
|
||||||
|
"or you must set GITHUB_REPOSITORY=<owner>/<repo> manually.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, repo] = repository.split("/");
|
||||||
|
|
||||||
|
if (!owner || !repo) {
|
||||||
|
throw new Error(
|
||||||
|
`GITHUB_REPOSITORY is malformed: "${repository}". Expected "<owner>/<repo>".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { owner, repo };
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { isAbsolute, resolve } from "node:path";
|
||||||
|
|
||||||
|
import type { FileChange, FileSource } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of repository-relative target paths to their content source.
|
||||||
|
*
|
||||||
|
* Keys are the destination paths to write inside the repository (e.g.
|
||||||
|
* "README.md", "config/license.txt"). Values describe where the content
|
||||||
|
* comes from — either inline or a local file path to read at runtime.
|
||||||
|
*/
|
||||||
|
export type FileSourceMap = Record<string, FileSource | string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a {@link FileSourceMap} into concrete {@link FileChange}s by
|
||||||
|
* loading any disk-backed entries.
|
||||||
|
*
|
||||||
|
* Resolution rules:
|
||||||
|
*
|
||||||
|
* - If a value is a plain string, it is treated as inline UTF-8 content
|
||||||
|
* (shorthand for `{ content: <string> }`). This preserves backwards
|
||||||
|
* compatibility with the original `Record<string, string>` shape.
|
||||||
|
* - If a value is `{ content }`, it is used verbatim.
|
||||||
|
* - If a value is `{ sourcePath }`, the file at that path is read from
|
||||||
|
* the local filesystem. Relative paths are resolved against `baseDir`
|
||||||
|
* (default: `process.cwd()`).
|
||||||
|
*
|
||||||
|
* Encoding handling for disk-backed sources:
|
||||||
|
*
|
||||||
|
* - `"utf-8"` (default): file is read as text and used as-is. The
|
||||||
|
* downstream commit pipeline will base64-encode it before sending to
|
||||||
|
* the GitHub GraphQL API.
|
||||||
|
* - `"base64"`: file is read as raw bytes and base64-encoded. Useful
|
||||||
|
* when you want to ship the file through unchanged but the calling
|
||||||
|
* code expects a string. Note that the commit pipeline will then
|
||||||
|
* base64-encode the *already-base64* string a second time, which is
|
||||||
|
* almost never what you want — prefer `"binary"` instead.
|
||||||
|
* - `"binary"`: file is read as raw bytes and base64-encoded once for
|
||||||
|
* transport. The commit pipeline detects this via the
|
||||||
|
* `preEncoded: true` marker and skips its own base64 step.
|
||||||
|
*
|
||||||
|
* Each promise rejection from `readFile` is wrapped with the offending
|
||||||
|
* path so failures are easy to diagnose in bulk runs.
|
||||||
|
*
|
||||||
|
* @param sources The file source map to resolve.
|
||||||
|
* @param baseDir Directory to resolve relative `sourcePath`s against.
|
||||||
|
* Defaults to `process.cwd()`.
|
||||||
|
*
|
||||||
|
* @returns A list of {@link FileChange}s ready to be handed to the
|
||||||
|
* commit service. Order matches the iteration order of the
|
||||||
|
* input map's keys.
|
||||||
|
*/
|
||||||
|
export async function resolveFileSources(
|
||||||
|
sources: FileSourceMap,
|
||||||
|
baseDir: string = process.cwd(),
|
||||||
|
): Promise<FileChange[]> {
|
||||||
|
const entries = Object.entries(sources);
|
||||||
|
|
||||||
|
const changes = await Promise.all(
|
||||||
|
entries.map(async ([path, source]) => loadOne(path, source, baseDir)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a single map entry into a {@link FileChange}, dispatching to
|
||||||
|
* the appropriate loader based on the source shape.
|
||||||
|
*/
|
||||||
|
async function loadOne(
|
||||||
|
path: string,
|
||||||
|
source: FileSource | string,
|
||||||
|
baseDir: string,
|
||||||
|
): Promise<FileChange> {
|
||||||
|
// Shorthand: bare string ⇒ inline content.
|
||||||
|
if (typeof source === "string") {
|
||||||
|
return { path, content: source };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline content branch.
|
||||||
|
if ("content" in source && source.content !== undefined) {
|
||||||
|
return { path, content: source.content };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk-backed branch.
|
||||||
|
if ("sourcePath" in source && source.sourcePath !== undefined) {
|
||||||
|
const absolute = isAbsolute(source.sourcePath)
|
||||||
|
? source.sourcePath
|
||||||
|
: resolve(baseDir, source.sourcePath);
|
||||||
|
|
||||||
|
const encoding = source.encoding ?? "utf-8";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (encoding === "binary") {
|
||||||
|
// Read raw bytes and base64-encode once for transport.
|
||||||
|
const buf = await readFile(absolute);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
content: buf.toString("base64"),
|
||||||
|
preEncoded: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === "base64") {
|
||||||
|
// Read raw bytes and surface as a base64 string. The commit
|
||||||
|
// pipeline will base64-encode this *again*; this branch exists
|
||||||
|
// only for callers that explicitly want that behaviour.
|
||||||
|
const buf = await readFile(absolute);
|
||||||
|
return { path, content: buf.toString("base64") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: UTF-8 text.
|
||||||
|
const text = await readFile(absolute, "utf-8");
|
||||||
|
return { path, content: text };
|
||||||
|
} catch (error) {
|
||||||
|
const reason = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load file source for "${path}" from "${absolute}": ${reason}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Invalid FileSource for "${path}": must provide either "content" or "sourcePath".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
export interface BranchInfo {
|
||||||
|
name: string;
|
||||||
|
headOid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single file write to apply on a branch.
|
||||||
|
*
|
||||||
|
* @property path Repository-relative path including any directories
|
||||||
|
* (e.g. "README.md", "config/license.txt").
|
||||||
|
* @property content UTF-8 file content. Will be base64-encoded before being
|
||||||
|
* sent to the GitHub GraphQL API.
|
||||||
|
*/
|
||||||
|
export interface FileChange {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
/**
|
||||||
|
* When true, `content` is already base64-encoded and ready for transport
|
||||||
|
* to the GitHub GraphQL API. The commit pipeline will skip its own
|
||||||
|
* base64 step for this entry.
|
||||||
|
*
|
||||||
|
* Used by binary file sources loaded from disk, where re-encoding the
|
||||||
|
* raw bytes as UTF-8 would corrupt them.
|
||||||
|
*/
|
||||||
|
preEncoded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarative description of where a file's contents come from.
|
||||||
|
*
|
||||||
|
* Either supply the content inline (`{ content: "..." }`), or point at a
|
||||||
|
* local file on disk that should be read at runtime (`{ sourcePath: "..." }`).
|
||||||
|
* Exactly one of `content` or `sourcePath` must be provided.
|
||||||
|
*
|
||||||
|
* @property content UTF-8 file content, supplied inline.
|
||||||
|
* @property sourcePath Path on the local filesystem to read the file
|
||||||
|
* contents from. Resolved relative to the current
|
||||||
|
* working directory unless absolute. The file is
|
||||||
|
* read as UTF-8.
|
||||||
|
* @property encoding Optional encoding override when reading from disk.
|
||||||
|
* Defaults to "utf-8". Use "binary" / "base64" for
|
||||||
|
* non-text files (the loader will base64-encode
|
||||||
|
* binary content directly without a UTF-8 round-trip).
|
||||||
|
*/
|
||||||
|
export type FileSource =
|
||||||
|
| { content: string; sourcePath?: never; encoding?: never }
|
||||||
|
| {
|
||||||
|
sourcePath: string;
|
||||||
|
content?: never;
|
||||||
|
encoding?: "utf-8" | "binary" | "base64";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single per-branch commit job, used as input to a batched
|
||||||
|
* `createCommitOnBranch` mutation.
|
||||||
|
*
|
||||||
|
* @property branchName The branch to commit to.
|
||||||
|
* @property expectedHeadOid The branch's current HEAD OID, used by GitHub
|
||||||
|
* for optimistic concurrency control.
|
||||||
|
* @property files One or more files to add/update in the commit.
|
||||||
|
* @property commitHeadline Commit message headline.
|
||||||
|
* @property commitBody Optional commit message body. Useful for
|
||||||
|
* including `Co-authored-by:` trailers, which
|
||||||
|
* GitHub renders as additional authors on the
|
||||||
|
* commit page.
|
||||||
|
*/
|
||||||
|
export interface BranchCommit {
|
||||||
|
branchName: string;
|
||||||
|
expectedHeadOid: string;
|
||||||
|
files: FileChange[];
|
||||||
|
commitHeadline: string;
|
||||||
|
commitBody?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtectionRule {
|
||||||
|
pattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateResult {
|
||||||
|
branch: string;
|
||||||
|
success: boolean;
|
||||||
|
commitOid?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphQLResponse<T = unknown> {
|
||||||
|
data?: T;
|
||||||
|
errors?: Array<{ message: string; type?: string; path?: string[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepoContext {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user