577 lines
20 KiB
JavaScript
577 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
import { execFileSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import { pathToFileURL } from "node:url";
|
||
|
||
const API_VERSION = "2022-11-28";
|
||
const DATABASE_LABEL_PREFIX = "db/";
|
||
const dryRun = process.env.DRY_RUN === "1" || process.env.DRY_RUN === "true";
|
||
|
||
export const STATIC_LABEL_SPECS = {
|
||
bug: { color: "d73a4a", description: "Fixes a bug" },
|
||
enhancement: { color: "a2eeef", description: "Adds or improves functionality" },
|
||
documentation: { color: "0075ca", description: "Documentation-only change" },
|
||
maintenance: { color: "ededed", description: "Maintenance, refactoring, build, CI, or test change" },
|
||
"area/desktop": { color: "1d76db", description: "Desktop application or Tauri shell" },
|
||
"area/core": { color: "5319e7", description: "Shared DBX core runtime" },
|
||
"area/web": { color: "0e8a16", description: "Web backend or web API" },
|
||
"area/agents": { color: "f9d0c4", description: "Database agents and agent runtime" },
|
||
"area/mcp": { color: "c2e0c6", description: "MCP server or packages" },
|
||
"area/cli": { color: "bfd4f2", description: "DBX command-line interface" },
|
||
"area/plugins": { color: "d4c5f9", description: "Dialect, JDBC, or mapping plugins" },
|
||
"area/docs": { color: "0075ca", description: "Documentation site or repository documentation" },
|
||
"area/ci": { color: "e99695", description: "GitHub Actions or repository automation" },
|
||
"area/deploy": { color: "fef2c0", description: "Deployment, packaging, or distribution" },
|
||
"area/multiple": { color: "ededed", description: "Touches more than three repository areas" },
|
||
"db/multiple": { color: "ededed", description: "Touches more than three database integrations" },
|
||
"ui-change": { color: "f9d0c4", description: "Changes user-visible interface, text, or visual assets" },
|
||
"dependencies/frontend": { color: "fbca04", description: "Adds a frontend dependency" },
|
||
"dependencies/backend": { color: "c2e0c6", description: "Adds a backend dependency" },
|
||
"tests-only": { color: "bfd4f2", description: "Changes only tests, fixtures, or snapshots" },
|
||
};
|
||
|
||
const TYPE_LABELS = new Set(["bug", "enhancement", "documentation", "maintenance"]);
|
||
const LABEL_PALETTE = [
|
||
"0e8a16",
|
||
"1d76db",
|
||
"5319e7",
|
||
"c2e0c6",
|
||
"bfd4f2",
|
||
"d4c5f9",
|
||
"fef2c0",
|
||
"fbca04",
|
||
"f9d0c4",
|
||
"e99695",
|
||
"f29513",
|
||
"c5def5",
|
||
];
|
||
|
||
const DRIVER_DATABASE_ALIASES = {
|
||
"cassandra-go": "cassandra",
|
||
gbase8a: "gbase",
|
||
gbase8s: "gbase",
|
||
"h2-legacy": "h2",
|
||
kafka: "mq",
|
||
"kingbase-go": "kingbase",
|
||
"neo4j-go": "neo4j",
|
||
"oracle-10g": "oracle",
|
||
"oracle-go": "oracle",
|
||
"oracle-legacy": "oracle",
|
||
rabbitmq: "mq",
|
||
rocketmq: "mq",
|
||
"sqlserver-legacy": "sqlserver",
|
||
"vastbase-go": "vastbase",
|
||
};
|
||
|
||
const DIALECT_DATABASE_ALIASES = {
|
||
oceanbase: "oceanbase-oracle",
|
||
postgresql: "postgres",
|
||
turso: "sqlite",
|
||
};
|
||
|
||
const DATABASE_PATH_TOKENS = [
|
||
[/clickhouse/i, "clickhouse"],
|
||
[/doris/i, "doris"],
|
||
[/duckdb/i, "duckdb"],
|
||
[/jdbc[x]?/i, "jdbc"],
|
||
[/mongo(?:db)?/i, "mongodb"],
|
||
[/mysql/i, "mysql"],
|
||
[/oracle/i, "oracle"],
|
||
[/postgres(?:ql)?/i, "postgres"],
|
||
[/redis/i, "redis"],
|
||
[/sqlite/i, "sqlite"],
|
||
[/sqlserver/i, "sqlserver"],
|
||
];
|
||
|
||
const PACKAGE_DEPENDENCY_SECTIONS = [
|
||
"dependencies",
|
||
"devDependencies",
|
||
"optionalDependencies",
|
||
"peerDependencies",
|
||
];
|
||
|
||
function stablePaletteColor(value) {
|
||
let hash = 0;
|
||
for (const character of value) {
|
||
hash = (hash * 31 + character.codePointAt(0)) >>> 0;
|
||
}
|
||
return LABEL_PALETTE[hash % LABEL_PALETTE.length];
|
||
}
|
||
|
||
function normalizeChangedFiles(changedFiles) {
|
||
return [...new Set(changedFiles.map((file) => String(file).replaceAll("\\", "/")).filter(Boolean))].sort();
|
||
}
|
||
|
||
function conventionalType(title) {
|
||
const normalized = String(title || "").trim().replace(/^[^\p{L}\p{N}]+/u, "");
|
||
return /^([a-z]+)(?:\([^)]*\))?!?[::]/i.exec(normalized)?.[1]?.toLowerCase() || null;
|
||
}
|
||
|
||
export function isDocumentationFile(file) {
|
||
return file.startsWith("docs/")
|
||
|| /(^|\/)README(?:\.[^/]+)?\.md$/i.test(file)
|
||
|| /(^|\/)(SECURITY|CONTRIBUTING|CODE_OF_CONDUCT)\.md$/i.test(file);
|
||
}
|
||
|
||
export function inferTypeLabel(title, changedFiles) {
|
||
const type = conventionalType(title);
|
||
if (type === "fix") return "bug";
|
||
if (type === "feat") return "enhancement";
|
||
if (type === "docs") return "documentation";
|
||
if (["build", "chore", "ci", "perf", "refactor", "style", "test"].includes(type)) return "maintenance";
|
||
if (changedFiles.length > 0 && changedFiles.every(isDocumentationFile)) return "documentation";
|
||
return null;
|
||
}
|
||
|
||
export function inferAreaLabels(changedFiles) {
|
||
const labels = new Set();
|
||
const has = (predicate) => changedFiles.some(predicate);
|
||
|
||
if (has((file) => file === "package.json"
|
||
|| file.startsWith("apps/desktop/")
|
||
|| file.startsWith("packages/app-tests/")
|
||
|| file.startsWith("src-tauri/"))) {
|
||
labels.add("area/desktop");
|
||
}
|
||
if (has((file) => file.startsWith("crates/dbx-core/") || file.startsWith("packages/node-core/"))) {
|
||
labels.add("area/core");
|
||
}
|
||
if (has((file) => file.startsWith("crates/dbx-web/") || file.startsWith("examples/web-api/"))) {
|
||
labels.add("area/web");
|
||
}
|
||
if (has((file) => file.startsWith("agents/"))) labels.add("area/agents");
|
||
if (has((file) => file.startsWith("crates/dbx-mcp/")
|
||
|| file.startsWith("packages/mcp-")
|
||
|| file.startsWith("packages/mcp-server/")
|
||
|| file.startsWith("examples/mcp/"))) {
|
||
labels.add("area/mcp");
|
||
}
|
||
if (has((file) => file.startsWith("crates/dbx-cli/")
|
||
|| file.startsWith("packages/cli-")
|
||
|| file.startsWith("packages/cli/")
|
||
|| file.startsWith("examples/cli/"))) {
|
||
labels.add("area/cli");
|
||
}
|
||
if (has((file) => file.startsWith("plugins/"))) labels.add("area/plugins");
|
||
if (has(isDocumentationFile)) labels.add("area/docs");
|
||
if (has((file) => file.startsWith(".github/"))) labels.add("area/ci");
|
||
if (has((file) => file.startsWith("deploy/")
|
||
|| file === "flake.nix"
|
||
|| file === "flake.lock"
|
||
|| file === "Makefile"
|
||
|| /^packages\/(?:cli|mcp)-(?:darwin|linux|win32)-/.test(file))) {
|
||
labels.add("area/deploy");
|
||
}
|
||
|
||
return [...labels].sort();
|
||
}
|
||
|
||
function isVisualAsset(file) {
|
||
return /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp|woff2?|ttf|otf)$/i.test(file);
|
||
}
|
||
|
||
export function isUiChangeFile(file) {
|
||
if (isTestFile(file)) return false;
|
||
|
||
if (file.startsWith("apps/desktop/")) {
|
||
return /\.(?:vue|css|less|sass|scss|styl)$/i.test(file)
|
||
|| file.startsWith("apps/desktop/src/components/")
|
||
|| file.startsWith("apps/desktop/src/i18n/")
|
||
|| file.startsWith("apps/desktop/src/styles/")
|
||
|| file.startsWith("apps/desktop/public/")
|
||
|| isVisualAsset(file);
|
||
}
|
||
|
||
if (file.startsWith("src-tauri/icons/")) return true;
|
||
|
||
if (file.startsWith("docs/")) {
|
||
return /\.(?:css|jsx|tsx)$/i.test(file)
|
||
|| file.startsWith("docs/components/")
|
||
|| (file.startsWith("docs/public/") && isVisualAsset(file));
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
export function isTestFile(file) {
|
||
return /(^|\/)(?:__tests__|fixtures|snapshots|test|tests|test-support)(\/|$)/.test(file)
|
||
|| /\.(?:spec|test)\.[^/]+$/i.test(file)
|
||
|| /(^|\/)packages\/app-tests\//.test(file);
|
||
}
|
||
|
||
function normalizeDatabaseType(value, aliases) {
|
||
const normalized = String(value || "").toLowerCase();
|
||
return aliases[normalized] || normalized;
|
||
}
|
||
|
||
export function inferDatabaseTypes(changedFiles, knownDatabaseTypes) {
|
||
const databaseTypes = new Set();
|
||
|
||
const add = (value) => {
|
||
if (knownDatabaseTypes.has(value)) databaseTypes.add(value);
|
||
};
|
||
|
||
for (const file of changedFiles) {
|
||
const driverMatch = /^agents\/drivers\/([^/]+)\//.exec(file);
|
||
if (driverMatch) add(normalizeDatabaseType(driverMatch[1], DRIVER_DATABASE_ALIASES));
|
||
|
||
const dialectMatch = /^plugins\/dialects\/([^/]+)\.ya?ml$/i.exec(file);
|
||
if (dialectMatch) add(normalizeDatabaseType(dialectMatch[1], DIALECT_DATABASE_ALIASES));
|
||
|
||
if (file.startsWith("plugins/jdbc/")) add("jdbc");
|
||
|
||
if (file.startsWith("apps/desktop/src/") || file.startsWith("crates/dbx-core/src/")) {
|
||
for (const [pattern, databaseType] of DATABASE_PATH_TOKENS) {
|
||
if (pattern.test(file)) add(databaseType);
|
||
}
|
||
}
|
||
}
|
||
|
||
return [...databaseTypes].sort();
|
||
}
|
||
|
||
export function parsePackageDependencyNames(source) {
|
||
if (!source) return new Set();
|
||
const manifest = JSON.parse(source);
|
||
const names = new Set();
|
||
for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
|
||
const dependencies = manifest[section];
|
||
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
|
||
for (const name of Object.keys(dependencies)) names.add(name);
|
||
}
|
||
return names;
|
||
}
|
||
|
||
function unquoteTomlKey(value) {
|
||
const trimmed = String(value || "").trim();
|
||
if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
|
||
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||
return trimmed.slice(1, -1);
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
export function parseCargoDependencyNames(source) {
|
||
const names = new Set();
|
||
let dependencySection = false;
|
||
|
||
for (const line of String(source || "").split(/\r?\n/)) {
|
||
const sectionMatch = /^\s*\[([^\]]+)]\s*(?:#.*)?$/.exec(line);
|
||
if (sectionMatch) {
|
||
const section = sectionMatch[1].trim();
|
||
const dependencyMatch = /(?:^|\.)(?:dev-|build-)?dependencies(?:\.(.+))?$/.exec(section);
|
||
dependencySection = Boolean(dependencyMatch && !dependencyMatch[1]);
|
||
if (dependencyMatch?.[1]) names.add(unquoteTomlKey(dependencyMatch[1].split(".")[0]));
|
||
continue;
|
||
}
|
||
|
||
if (!dependencySection || /^\s*(?:#|$)/.test(line)) continue;
|
||
const keyMatch = /^\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*=/.exec(line);
|
||
const name = keyMatch?.[1] || keyMatch?.[2] || keyMatch?.[3];
|
||
if (name) names.add(name);
|
||
}
|
||
|
||
return names;
|
||
}
|
||
|
||
function normalizeGradleDependency(value) {
|
||
const coordinate = String(value || "").trim();
|
||
if (!coordinate) return null;
|
||
if (coordinate.startsWith(":")) return `project:${coordinate}`;
|
||
const parts = coordinate.split(":");
|
||
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : coordinate;
|
||
}
|
||
|
||
export function parseGradleDependencyNames(source) {
|
||
const names = new Set();
|
||
const configuration = /^(?:api|annotationProcessor|classpath|compileOnly|implementation|kapt|ksp|runtimeOnly|testCompileOnly|testImplementation|testRuntimeOnly)$/;
|
||
|
||
for (const line of String(source || "").split(/\r?\n/)) {
|
||
const declaration = /^\s*([A-Za-z][A-Za-z0-9]*)\b(.*)$/.exec(line);
|
||
if (!declaration || !configuration.test(declaration[1])) continue;
|
||
const rest = declaration[2].replace(/\/\/.*$/, "");
|
||
const quoted = /['"]([^'"]+)['"]/.exec(rest)?.[1];
|
||
const catalog = /\b(libs\.[A-Za-z0-9_.-]+)/.exec(rest)?.[1];
|
||
const name = catalog ? `catalog:${catalog}` : normalizeGradleDependency(quoted);
|
||
if (name) names.add(name);
|
||
}
|
||
|
||
return names;
|
||
}
|
||
|
||
export function parseGoDependencyNames(source) {
|
||
const names = new Set();
|
||
let inRequireBlock = false;
|
||
|
||
for (const line of String(source || "").split(/\r?\n/)) {
|
||
const trimmed = line.replace(/\/\/.*$/, "").trim();
|
||
if (trimmed === "require (") {
|
||
inRequireBlock = true;
|
||
continue;
|
||
}
|
||
if (inRequireBlock && trimmed === ")") {
|
||
inRequireBlock = false;
|
||
continue;
|
||
}
|
||
|
||
const value = inRequireBlock ? trimmed : /^require\s+(.+)$/.exec(trimmed)?.[1];
|
||
const moduleName = value?.split(/\s+/)[0];
|
||
if (moduleName) names.add(moduleName);
|
||
}
|
||
|
||
return names;
|
||
}
|
||
|
||
export function parseMavenDependencyNames(source) {
|
||
const names = new Set();
|
||
for (const match of String(source || "").matchAll(/<dependency>\s*([\s\S]*?)<\/dependency>/g)) {
|
||
const groupId = /<groupId>\s*([^<]+?)\s*<\/groupId>/.exec(match[1])?.[1];
|
||
const artifactId = /<artifactId>\s*([^<]+?)\s*<\/artifactId>/.exec(match[1])?.[1];
|
||
if (groupId && artifactId) names.add(`${groupId}:${artifactId}`);
|
||
}
|
||
return names;
|
||
}
|
||
|
||
function dependencySide(file) {
|
||
if (file === "package.json" || file === "docs/package.json" || file === "packages/mongo-shell/package.json") {
|
||
return "frontend";
|
||
}
|
||
if (/^packages\/(?:cli(?:-|\/)|mcp(?:-|server\/)).*\/package\.json$/.test(file)
|
||
|| file === "packages/cli/package.json"
|
||
|| file === "packages/mcp-server/package.json") {
|
||
return "backend";
|
||
}
|
||
if (file === "Cargo.toml"
|
||
|| file.endsWith("/Cargo.toml")
|
||
|| file.endsWith("/build.gradle")
|
||
|| file.endsWith("/build.gradle.kts")
|
||
|| file.endsWith("/go.mod")
|
||
|| file.endsWith("/pom.xml")) {
|
||
return "backend";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function parseDependencyNames(file, source) {
|
||
if (file.endsWith("package.json")) return parsePackageDependencyNames(source);
|
||
if (file === "Cargo.toml" || file.endsWith("/Cargo.toml")) return parseCargoDependencyNames(source);
|
||
if (file.endsWith("/build.gradle") || file.endsWith("/build.gradle.kts")) return parseGradleDependencyNames(source);
|
||
if (file.endsWith("/go.mod")) return parseGoDependencyNames(source);
|
||
if (file.endsWith("/pom.xml")) return parseMavenDependencyNames(source);
|
||
return new Set();
|
||
}
|
||
|
||
export function inferAddedDependencies(changedFiles, readBaseFile, readHeadFile) {
|
||
const added = { frontend: new Set(), backend: new Set() };
|
||
|
||
for (const file of changedFiles) {
|
||
const side = dependencySide(file);
|
||
if (!side) continue;
|
||
|
||
const baseNames = parseDependencyNames(file, readBaseFile(file) || "");
|
||
const headNames = parseDependencyNames(file, readHeadFile(file) || "");
|
||
for (const name of headNames) {
|
||
if (!baseNames.has(name)) added[side].add(`${file}:${name}`);
|
||
}
|
||
}
|
||
|
||
return added;
|
||
}
|
||
|
||
export function evaluatePullRequestLabels({
|
||
title,
|
||
changedFiles,
|
||
knownDatabaseTypes,
|
||
readBaseFile = () => null,
|
||
readHeadFile = () => null,
|
||
}) {
|
||
const files = normalizeChangedFiles(changedFiles);
|
||
const areaLabels = inferAreaLabels(files);
|
||
const labels = new Set(areaLabels.length > 3 ? ["area/multiple"] : areaLabels);
|
||
const typeLabel = inferTypeLabel(title, files);
|
||
if (typeLabel) labels.add(typeLabel);
|
||
if (files.some(isUiChangeFile)) labels.add("ui-change");
|
||
if (files.length > 0 && files.every(isTestFile)) labels.add("tests-only");
|
||
|
||
const databaseTypes = inferDatabaseTypes(files, knownDatabaseTypes);
|
||
if (databaseTypes.length > 3) {
|
||
labels.add("db/multiple");
|
||
} else {
|
||
for (const databaseType of databaseTypes) labels.add(`${DATABASE_LABEL_PREFIX}${databaseType}`);
|
||
}
|
||
|
||
const addedDependencies = inferAddedDependencies(files, readBaseFile, readHeadFile);
|
||
if (addedDependencies.frontend.size > 0) labels.add("dependencies/frontend");
|
||
if (addedDependencies.backend.size > 0) labels.add("dependencies/backend");
|
||
|
||
return {
|
||
addedDependencies: {
|
||
frontend: [...addedDependencies.frontend].sort(),
|
||
backend: [...addedDependencies.backend].sort(),
|
||
},
|
||
databaseTypes,
|
||
files,
|
||
labels: [...labels].sort(),
|
||
typeLabel,
|
||
};
|
||
}
|
||
|
||
function labelNames(labels) {
|
||
return (labels || []).map((label) => (typeof label === "string" ? label : label.name)).filter(Boolean);
|
||
}
|
||
|
||
function loadEvent() {
|
||
if (!process.env.GITHUB_EVENT_PATH || !fs.existsSync(process.env.GITHUB_EVENT_PATH)) {
|
||
throw new Error("GITHUB_EVENT_PATH is required");
|
||
}
|
||
return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||
}
|
||
|
||
function loadDatabaseCatalog() {
|
||
const manifestUrl = new URL("../../crates/dbx-core/assets/database-drivers.manifest.json", import.meta.url);
|
||
const manifest = JSON.parse(fs.readFileSync(manifestUrl, "utf8"));
|
||
return new Map(manifest.drivers.map((driver) => [driver.dbType, driver.label]));
|
||
}
|
||
|
||
function listChangedFiles(baseSha, headSha) {
|
||
const output = execFileSync("git", ["diff", "--name-only", "-z", `${baseSha}...${headSha}`], {
|
||
encoding: "utf8",
|
||
maxBuffer: 16 * 1024 * 1024,
|
||
});
|
||
return output.split("\0").filter(Boolean);
|
||
}
|
||
|
||
function readFileAtRef(ref, file) {
|
||
try {
|
||
return execFileSync("git", ["show", `${ref}:${file}`], {
|
||
encoding: "utf8",
|
||
maxBuffer: 16 * 1024 * 1024,
|
||
stdio: ["ignore", "pipe", "ignore"],
|
||
});
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function githubRequest(method, path, body) {
|
||
const token = process.env.GITHUB_TOKEN;
|
||
const repository = process.env.GITHUB_REPOSITORY;
|
||
if (!token) throw new Error("GITHUB_TOKEN is required");
|
||
if (!repository) throw new Error("GITHUB_REPOSITORY is required");
|
||
|
||
const response = await fetch(`https://api.github.com/repos/${repository}${path}`, {
|
||
method,
|
||
headers: {
|
||
Accept: "application/vnd.github+json",
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
"X-GitHub-Api-Version": API_VERSION,
|
||
},
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
|
||
const text = await response.text();
|
||
const payload = text ? JSON.parse(text) : null;
|
||
if (!response.ok) {
|
||
const message = payload?.message || response.statusText;
|
||
const error = new Error(`${method} ${path} failed: ${response.status} ${message}`);
|
||
error.status = response.status;
|
||
error.payload = payload;
|
||
throw error;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
async function ensureLabel(name, spec) {
|
||
try {
|
||
await githubRequest("POST", "/labels", { name, color: spec.color, description: spec.description });
|
||
console.log(`Created label ${name}`);
|
||
} catch (error) {
|
||
if (error.status === 422) return;
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function addLabels(pullRequestNumber, names) {
|
||
await githubRequest("POST", `/issues/${pullRequestNumber}/labels`, { labels: names });
|
||
}
|
||
|
||
async function removeLabel(pullRequestNumber, name) {
|
||
try {
|
||
await githubRequest("DELETE", `/issues/${pullRequestNumber}/labels/${encodeURIComponent(name)}`);
|
||
} catch (error) {
|
||
if (error.status === 404) return;
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
function isManagedLabel(name) {
|
||
return TYPE_LABELS.has(name)
|
||
|| name.startsWith("area/")
|
||
|| name.startsWith(DATABASE_LABEL_PREFIX)
|
||
|| name.startsWith("dependencies/")
|
||
|| name === "tests-only"
|
||
|| name === "ui-change";
|
||
}
|
||
|
||
async function main() {
|
||
const event = loadEvent();
|
||
const pullRequest = event.pull_request;
|
||
if (!pullRequest?.number) throw new Error("Pull request event is required");
|
||
|
||
const baseSha = pullRequest.base?.sha;
|
||
const headSha = pullRequest.head?.sha;
|
||
if (!baseSha || !headSha) throw new Error("Pull request base and head SHAs are required");
|
||
|
||
const databaseCatalog = loadDatabaseCatalog();
|
||
const result = evaluatePullRequestLabels({
|
||
title: pullRequest.title || "",
|
||
changedFiles: listChangedFiles(baseSha, headSha),
|
||
knownDatabaseTypes: new Set(databaseCatalog.keys()),
|
||
readBaseFile: (file) => readFileAtRef(baseSha, file),
|
||
readHeadFile: (file) => readFileAtRef(headSha, file),
|
||
});
|
||
const existingLabels = labelNames(pullRequest.labels);
|
||
const labelsToAdd = result.labels.filter((name) => !existingLabels.includes(name));
|
||
const labelsToRemove = existingLabels.filter((name) => isManagedLabel(name) && !result.labels.includes(name));
|
||
|
||
const labelSpecs = { ...STATIC_LABEL_SPECS };
|
||
for (const databaseType of result.databaseTypes) {
|
||
labelSpecs[`${DATABASE_LABEL_PREFIX}${databaseType}`] = {
|
||
color: stablePaletteColor(databaseType),
|
||
description: `Database: ${databaseCatalog.get(databaseType) || databaseType}`,
|
||
};
|
||
}
|
||
|
||
console.log(JSON.stringify({
|
||
pullRequest: pullRequest.number,
|
||
title: pullRequest.title,
|
||
...result,
|
||
existingLabels,
|
||
labelsToAdd,
|
||
labelsToRemove,
|
||
}, null, 2));
|
||
|
||
if (dryRun) {
|
||
console.log("Dry run enabled; no GitHub API calls were made");
|
||
return;
|
||
}
|
||
|
||
for (const name of labelsToAdd) {
|
||
await ensureLabel(name, labelSpecs[name]);
|
||
}
|
||
|
||
if (labelsToAdd.length > 0) {
|
||
await addLabels(pullRequest.number, labelsToAdd);
|
||
console.log(`Added labels: ${labelsToAdd.join(", ")}`);
|
||
}
|
||
|
||
for (const name of labelsToRemove) {
|
||
await removeLabel(pullRequest.number, name);
|
||
console.log(`Removed stale label ${name}`);
|
||
}
|
||
}
|
||
|
||
const entryPoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
|
||
if (entryPoint === import.meta.url) await main();
|