diff --git a/.github/workflows/validate-new-gap-number.yml b/.github/workflows/validate-new-gap-number.yml new file mode 100644 index 0000000..1806e79 --- /dev/null +++ b/.github/workflows/validate-new-gap-number.yml @@ -0,0 +1,28 @@ +name: Validate new GAP number + +on: + pull_request: + branches: [main] + +jobs: + validate-gap-number: + name: Validate new GAP id matches PR number + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Validate new GAP id matches PR number + run: node scripts/validate-new-gap-number.js --pr-number ${{ github.event.pull_request.number }} diff --git a/scripts/validate-new-gap-number.js b/scripts/validate-new-gap-number.js new file mode 100755 index 0000000..331ba62 --- /dev/null +++ b/scripts/validate-new-gap-number.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +/** + * Validates that newly added GAP directories and their metadata.yml `id` field + * both match the PR number, per CONTRIBUTING.md. + * + * Usage: node scripts/validate-new-gap-number.js --pr-number 123 + */ + +import { readFileSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { dirname, basename } from "node:path"; +import { parseArgs } from "node:util"; +import { parse as parseYaml } from "yaml"; + +const { values } = parseArgs({ + options: { + "pr-number": { type: "string" }, + }, + strict: true, +}); + +const prNumber = values["pr-number"]; + +const newMetadataFiles = execSync( + "git diff --name-only --diff-filter=A origin/main...HEAD -- 'gaps/*/metadata.yml'", + { encoding: "utf8" }, +) + .trim() + .split("\n") + .filter(Boolean); + +if (newMetadataFiles.length === 0) { + console.log("No new metadata.yml files added in this PR."); + process.exit(0); +} + +let failed = false; + +for (const file of newMetadataFiles) { + const gapDir = basename(dirname(file)); + const expectedDir = `GAP-${prNumber}`; + + if (gapDir !== expectedDir) { + console.error( + `::error file=${file}::GAP directory is ${gapDir}, but must be ${expectedDir} to match the PR number. See CONTRIBUTING.md.`, + ); + failed = true; + } else { + console.log(`✓ ${file}: directory ${gapDir} matches PR #${prNumber}`); + } + + const content = readFileSync(file, "utf8"); + const metadata = parseYaml(content); + const id = String(metadata.id); + + if (id !== prNumber) { + console.error( + `::error file=${file}::metadata.yml 'id' field is ${id}, but must match the PR number (${prNumber}). See CONTRIBUTING.md.`, + ); + failed = true; + } else { + console.log(`✓ ${file}: id ${id} matches PR #${prNumber}`); + } +} + +if (failed) { + process.exit(1); +}