From ec72fda21cb9b2bc4b96281cb4b1db9c7937f3a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 07:31:05 +0100 Subject: [PATCH] refactor(scripts): use dependency metadata parsers --- scripts/gh-read.ts | 31 ++++++--------------- scripts/label-open-issues.ts | 30 ++------------------ scripts/lib/github-repo.ts | 31 +++++++++++++++++++++ scripts/sync-labels.ts | 53 ++++-------------------------------- test/scripts/gh-read.test.ts | 2 ++ 5 files changed, 49 insertions(+), 98 deletions(-) create mode 100644 scripts/lib/github-repo.ts diff --git a/scripts/gh-read.ts b/scripts/gh-read.ts index 060ccd23ed5c..555b0b2453fb 100644 --- a/scripts/gh-read.ts +++ b/scripts/gh-read.ts @@ -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; } diff --git a/scripts/label-open-issues.ts b/scripts/label-open-issues.ts index b30660b74f72..65402ba74b2c 100644 --- a/scripts/label-open-issues.ts +++ b/scripts/label-open-issues.ts @@ -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 { diff --git a/scripts/lib/github-repo.ts b/scripts/lib/github-repo.ts new file mode 100644 index 000000000000..e50049024e8b --- /dev/null +++ b/scripts/lib/github-repo.ts @@ -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; +} diff --git a/scripts/sync-labels.ts b/scripts/sync-labels.ts index 968429c8122f..736e67ac0ff2 100644 --- a/scripts/sync-labels.ts +++ b/scripts/sync-labels.ts @@ -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; +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 { const raw = execFileSync("gh", ["api", `repos/${repoLocal}/labels?per_page=100`, "--paginate"], { encoding: "utf8", diff --git a/test/scripts/gh-read.test.ts b/test/scripts/gh-read.test.ts index 2be3e67f8ebb..6e54d1ba419c 100644 --- a/test/scripts/gh-read.test.ts +++ b/test/scripts/gh-read.test.ts @@ -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(); });