| |
| |
| |
| |
|
|
| import { execFileSync } from "node:child_process";
|
|
|
| function runJson(cmd, args) {
|
| const out = execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
| try {
|
| return JSON.parse(out);
|
| } catch (e) {
|
| throw new Error(
|
| `Failed to parse JSON from ${cmd}: ${e.message}\nOutput (first 200 chars): ${out.slice(0, 200)}`
|
| );
|
| }
|
| }
|
|
|
| function runText(cmd, args) {
|
| return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
| }
|
|
|
| export function ghIssueListNumbers(owner, repo, label) {
|
| return runJson("gh", [
|
| "issue",
|
| "list",
|
| "--repo",
|
| `${owner}/${repo}`,
|
| "--state",
|
| "open",
|
| "-l",
|
| label,
|
| "--limit",
|
| "500",
|
| "--json",
|
| "number",
|
| ]).map((x) => x.number);
|
| }
|
|
|
| export function ghIssueListFeatureTitled(owner, repo) {
|
| const items = runJson("gh", [
|
| "issue",
|
| "list",
|
| "--repo",
|
| `${owner}/${repo}`,
|
| "--state",
|
| "open",
|
| "--limit",
|
| "500",
|
| "--json",
|
| "number,title",
|
| ]);
|
| return items.filter((i) => /\[feature\]|feature request/i.test(i.title)).map((i) => i.number);
|
| }
|
|
|
| export function ghIssueView(owner, repo, number) {
|
| return runJson("gh", [
|
| "issue",
|
| "view",
|
| String(number),
|
| "--repo",
|
| `${owner}/${repo}`,
|
| "--json",
|
| "number,title,url,body,state,stateReason,labels,author,assignees,createdAt,closedAt,comments,reactionGroups",
|
| ]);
|
| }
|
|
|
| export function ghPrSearchOpen(owner, repo, issueNumber) {
|
| return runJson("gh", [
|
| "pr",
|
| "list",
|
| "--repo",
|
| `${owner}/${repo}`,
|
| "--state",
|
| "open",
|
| "--search",
|
| `#${issueNumber}`,
|
| "--json",
|
| "number,title,body",
|
| "--limit",
|
| "10",
|
| ]);
|
| }
|
|
|
| export function ghPrSearchMerged(owner, repo, issueNumber) {
|
| return runJson("gh", [
|
| "pr",
|
| "list",
|
| "--repo",
|
| `${owner}/${repo}`,
|
| "--state",
|
| "merged",
|
| "--search",
|
| `#${issueNumber}`,
|
| "--json",
|
| "number,title,body,mergedAt,mergeCommit",
|
| "--limit",
|
| "20",
|
| ]);
|
| }
|
|
|
| export function gitTagsByDate() {
|
| const out = runText("git", [
|
| "tag",
|
| "--sort=creatordate",
|
| "--format=%(creatordate:iso8601)|%(refname:short)",
|
| ]);
|
| return out
|
| .split("\n")
|
| .filter(Boolean)
|
| .map((line) => {
|
| const [date, name] = line.split("|");
|
| return { date: new Date(date), name };
|
| })
|
| .filter((t) => /^v\d+\.\d+\.\d+$/.test(t.name));
|
| }
|
|
|
| export function gitLogGrep(pattern) {
|
| try {
|
| const out = runText("git", [
|
| "log",
|
| "--all",
|
| `--grep=${pattern}`,
|
| "--format=%H|%cI|%s",
|
| "--regexp-ignore-case",
|
| ]);
|
| if (!out) return [];
|
| return out.split("\n").map((line) => {
|
| const [hash, date, ...rest] = line.split("|");
|
| return { hash, date: new Date(date), subject: rest.join("|") };
|
| });
|
| } catch {
|
| return [];
|
| }
|
| }
|
|
|
| export function gitIsAncestor(hash, ref) {
|
| try {
|
| execFileSync("git", ["merge-base", "--is-ancestor", hash, ref], { stdio: "ignore" });
|
| return true;
|
| } catch {
|
| return false;
|
| }
|
| }
|
|
|
| export function gitCurrentReleaseBranch() {
|
| try {
|
| const out = runText("git", ["branch", "--format=%(refname:short)"]);
|
| const branches = out.split("\n").filter((b) => /^release\/v\d+\.\d+\.\d+$/.test(b));
|
| if (branches.length === 0) return null;
|
| const current = runText("git", ["branch", "--show-current"]);
|
| if (branches.includes(current)) return current;
|
| branches.sort((a, b) => {
|
| const av = a.replace("release/v", "").split(".").map(Number);
|
| const bv = b.replace("release/v", "").split(".").map(Number);
|
| for (let i = 0; i < 3; i++) {
|
| if (av[i] !== bv[i]) return bv[i] - av[i];
|
| }
|
| return 0;
|
| });
|
| return branches[0];
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| export const defaultDeps = {
|
| ghIssueListNumbers,
|
| ghIssueListFeatureTitled,
|
| ghIssueView,
|
| ghPrSearchMerged,
|
| ghPrSearchOpen,
|
| gitTagsByDate,
|
| gitLogGrep,
|
| gitIsAncestor,
|
| gitCurrentReleaseBranch,
|
| };
|
|
|