refactor(scripts): use dependency metadata parsers

This commit is contained in:
Peter Steinberger
2026-07-13 23:33:30 -07:00
parent 3c82249ab9
commit ec72fda21c
5 changed files with 49 additions and 98 deletions
+8 -23
View File
@@ -1,11 +1,17 @@
// Gh Read script supports OpenClaw repository automation.
import { execFileSync, spawnSync } from "node:child_process";
import { spawnSync } from "node:child_process";
import { createPrivateKey, createSign } from "node:crypto";
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { readBoundedResponseText } from "./lib/bounded-response.ts";
import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
import {
normalizeGitHubRepo as normalizeRepo,
resolveGitHubRepoFromOrigin,
} from "./lib/github-repo.ts";
export { normalizeRepo };
const APP_ID_ENV = "OPENCLAW_GH_READ_APP_ID";
const KEY_FILE_ENV = "OPENCLAW_GH_READ_PRIVATE_KEY_FILE";
@@ -65,23 +71,6 @@ export function parseRepoArg(args: string[]): string | null {
return null;
}
export function normalizeRepo(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
const withoutProtocol = trimmed.replace(/^[a-z]+:\/\//i, "");
const withoutHost = withoutProtocol.replace(/^(?:[^@/]+@)?github\.com[:/]/i, "");
const normalized = withoutHost.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
const parts = normalized.split("/").filter(Boolean);
if (parts.length < 2) {
return null;
}
return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
}
export function parsePermissionKeys(raw: string | null | undefined): string[] {
const trimmed = raw?.trim();
if (!trimmed) {
@@ -147,11 +136,7 @@ function resolveRepo(args: string[]): string | null {
}
try {
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
return normalizeRepo(remote);
return resolveGitHubRepoFromOrigin();
} catch {
return null;
}
+3 -27
View File
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
import { isRecord } from "../src/utils.js";
import { readBoundedResponseText as readBoundedBodyText } from "./lib/bounded-response.ts";
import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
import { resolveGitHubRepoFromOrigin } from "./lib/github-repo.ts";
function writeStdoutLine(message = ""): void {
process.stdout.write(`${message}\n`);
@@ -466,33 +467,8 @@ function runGh(args: string[]): string {
}
function resolveRepo(): RepoInfo {
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
encoding: "utf8",
}).trim();
if (!remote) {
throw new Error("Unable to determine repository from git remote.");
}
const normalized = remote.replace(/\.git$/, "");
if (normalized.startsWith("git@github.com:")) {
const slug = normalized.replace("git@github.com:", "");
const [owner, name] = slug.split("/");
if (owner && name) {
return { owner, name };
}
}
if (normalized.startsWith("https://github.com/")) {
const slug = normalized.replace("https://github.com/", "");
const [owner, name] = slug.split("/");
if (owner && name) {
return { owner, name };
}
}
throw new Error(`Unsupported GitHub remote: ${remote}`);
const [owner, name] = resolveGitHubRepoFromOrigin().split("/") as [string, string];
return { owner, name };
}
function fetchIssuePage(repo: RepoInfo, after: string | null): IssuePage {
+31
View File
@@ -0,0 +1,31 @@
import { execFileSync } from "node:child_process";
import hostedGitInfo from "hosted-git-info";
export function normalizeGitHubRepo(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
const candidate = /^github\.com\//i.test(trimmed) ? `https://${trimmed}` : trimmed;
const info = hostedGitInfo.fromUrl(candidate);
return info?.type === "github" && info.user && info.project
? `${info.user}/${info.project}`
: null;
}
export function resolveGitHubRepoFromOrigin(): string {
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (!remote) {
throw new Error("Unable to determine repository from git remote.");
}
const repo = normalizeGitHubRepo(remote);
if (!repo) {
throw new Error(`Unsupported GitHub remote: ${remote}`);
}
return repo;
}
+5 -48
View File
@@ -2,6 +2,8 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { parse } from "yaml";
import { resolveGitHubRepoFromOrigin } from "./lib/github-repo.ts";
type RepoLabel = {
name: string;
@@ -48,15 +50,10 @@ const EXTRA_LABELS = [
"size: XL",
"beta-blocker",
] as const;
const labelNames = [
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
];
const labelerConfig = parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
const labelNames = [...new Set([...Object.keys(labelerConfig), ...EXTRA_LABELS])];
if (!labelNames.length) {
throw new Error("labeler.yml must declare at least one label.");
}
const repo = resolveRepo();
const repo = resolveGitHubRepoFromOrigin();
const existing = fetchExistingLabels(repo);
const missing = labelNames.filter((label) => !existing.has(label));
@@ -84,26 +81,6 @@ for (const label of missing) {
console.log(`Created label: ${label}`);
}
function extractLabelNames(contents: string): string[] {
const labels: string[] = [];
for (const line of contents.split("\n")) {
if (!line.trim() || line.trimStart().startsWith("#")) {
continue;
}
if (/^\s/.test(line)) {
continue;
}
const match = line.match(/^(["'])(.+)\1\s*:/) ?? line.match(/^([^:]+):/);
if (match) {
const name = (match[2] ?? match[1] ?? "").trim();
if (name) {
labels.push(name);
}
}
}
return labels;
}
function resolveLabelMetadata(label: string): { color: string; description?: string } {
const extraMetadata = EXTRA_LABEL_METADATA.get(label);
if (extraMetadata) {
@@ -113,26 +90,6 @@ function resolveLabelMetadata(label: string): { color: string; description?: str
return { color: COLOR_BY_PREFIX.get(prefix) ?? "ededed" };
}
function resolveRepo(): string {
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
encoding: "utf8",
}).trim();
if (!remote) {
throw new Error("Unable to determine repository from git remote.");
}
if (remote.startsWith("git@github.com:")) {
return remote.replace("git@github.com:", "").replace(/\.git$/, "");
}
if (remote.startsWith("https://github.com/")) {
return remote.replace("https://github.com/", "").replace(/\.git$/, "");
}
throw new Error(`Unsupported GitHub remote: ${remote}`);
}
function fetchExistingLabels(repoLocal: string): Map<string, RepoLabel> {
const raw = execFileSync("gh", ["api", `repos/${repoLocal}/labels?per_page=100`, "--paginate"], {
encoding: "utf8",
+2
View File
@@ -42,8 +42,10 @@ describe("gh-read helpers", () => {
it("normalizes repo strings from common git formats", () => {
expect(normalizeRepo("openclaw/openclaw")).toBe("openclaw/openclaw");
expect(normalizeRepo("github.com/openclaw/openclaw")).toBe("openclaw/openclaw");
expect(normalizeRepo("github:openclaw/openclaw")).toBe("openclaw/openclaw");
expect(normalizeRepo("https://github.com/openclaw/openclaw.git")).toBe("openclaw/openclaw");
expect(normalizeRepo("git@github.com:openclaw/openclaw.git")).toBe("openclaw/openclaw");
expect(normalizeRepo("https://gitlab.com/openclaw/openclaw.git")).toBeNull();
expect(normalizeRepo("invalid")).toBeNull();
});