From eea6cbc43a0f17c8e655a9c4be9074b027e2ba14 Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Tue, 25 Aug 2026 17:10:37 -0400 Subject: [PATCH 1/4] feat: add reusable PR description check workflow Add callable and local workflows plus Overview validation script so repos can enforce the org PR template via UseAlloy/.github. --- .github/scripts/validate-pr-description.js | 48 +++++++++++++++++++ .../callable.pr-description-check.yaml | 43 +++++++++++++++++ .../workflows/local.pr-description-check.yaml | 16 +++++++ 3 files changed, 107 insertions(+) create mode 100644 .github/scripts/validate-pr-description.js create mode 100644 .github/workflows/callable.pr-description-check.yaml create mode 100644 .github/workflows/local.pr-description-check.yaml diff --git a/.github/scripts/validate-pr-description.js b/.github/scripts/validate-pr-description.js new file mode 100644 index 0000000..3c6cf11 --- /dev/null +++ b/.github/scripts/validate-pr-description.js @@ -0,0 +1,48 @@ +/** + * Validate that a PR description has a filled Overview section. + * Intended for use with actions/github-script. + * + * @param {{ core: import('@actions/core'), context: import('@actions/github').Context }} params + * @param {{ minOverviewLength?: number }} options + */ +module.exports = async function validatePrDescription({ core, context }, options = {}) { + const minOverviewLength = Number(options.minOverviewLength) || 40; + const pr = context.payload.pull_request; + const body = pr?.body ?? ''; + + if (!body.trim()) { + core.setFailed('PR description is empty. Please add a description with enough context for reviewers.'); + return; + } + + if (/\*\*REPLACE ME\*\*/i.test(body)) { + core.setFailed('PR description still contains the "REPLACE ME" placeholder. Please fill in the overview.'); + return; + } + + const withoutComments = body.replace(//g, ''); + const overviewSection = withoutComments + .split(/\n(?=## )/) + .find((section) => /^##[^\n]*overview/i.test(section.trim())); + + if (!overviewSection) { + core.setFailed( + 'PR description must include an "## Overview" section. Use the standard PR template and fill in the overview.' + ); + return; + } + + const overviewText = overviewSection + .replace(/^##[^\n]*\n?/i, '') + .replace(/\s+/g, ' ') + .trim(); + + if (overviewText.length < minOverviewLength) { + core.setFailed( + `Overview section is too short (${overviewText.length} characters, minimum ${minOverviewLength}). Please add a brief description of the change.` + ); + return; + } + + core.info(`PR description check passed (overview: ${overviewText.length} characters).`); +}; diff --git a/.github/workflows/callable.pr-description-check.yaml b/.github/workflows/callable.pr-description-check.yaml new file mode 100644 index 0000000..355eea7 --- /dev/null +++ b/.github/workflows/callable.pr-description-check.yaml @@ -0,0 +1,43 @@ +name: pr-description-check +on: + workflow_call: + inputs: + min_overview_length: + description: Minimum character count required in the Overview section after stripping HTML comments and placeholders. + type: number + required: false + default: 40 + secrets: + GH_TOKEN: + required: true + +jobs: + pr-description-check: + name: PR description check + runs-on: ubuntu-latest + steps: + - name: Checkout workflow scripts + uses: actions/checkout@v7 + with: + # Check out the reusable workflow's own repo/SHA so callers get the + # script version that matches the pinned callable workflow. + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + sparse-checkout: | + .github/scripts + token: ${{ secrets.GH_TOKEN }} + + - name: Validate PR description + uses: actions/github-script@v9 + env: + MIN_OVERVIEW_LENGTH: ${{ inputs.min_overview_length }} + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const script = require('${{ github.workspace }}/.github/scripts/validate-pr-description.js'); + // github, context, and core are injected by github-script from this + // job's runtime context (the caller's workflow run / PR payload). + await script( + { github, context, core }, + { minOverviewLength: Number(process.env.MIN_OVERVIEW_LENGTH) || 40 } + ); diff --git a/.github/workflows/local.pr-description-check.yaml b/.github/workflows/local.pr-description-check.yaml new file mode 100644 index 0000000..12ab458 --- /dev/null +++ b/.github/workflows/local.pr-description-check.yaml @@ -0,0 +1,16 @@ +name: pr-description-check +on: + pull_request: + types: + - opened + - edited + - reopened + - labeled + - unlabeled + + +jobs: + call-pr-description-check-workflow: + uses: ./.github/workflows/callable.pr-description-check.yaml + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 113dfbd0fee5f01ead7a6d87b2f60e5a87cd2cee Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Tue, 25 Aug 2026 17:46:21 -0400 Subject: [PATCH 2/4] feat: exempt revert PRs and document Overview section requirement Revert PRs skip description validation in-script so required checks still pass. Add template comment noting the Overview section is validated by CI. --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/scripts/validate-pr-description.js | 19 ++++++++++++++++++- .../callable.pr-description-check.yaml | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b3c24ef..02a9507 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ ## ℹ️ Overview + **REPLACE ME**: Provide the context and description of the change. diff --git a/.github/scripts/validate-pr-description.js b/.github/scripts/validate-pr-description.js index 3c6cf11..5c05522 100644 --- a/.github/scripts/validate-pr-description.js +++ b/.github/scripts/validate-pr-description.js @@ -5,9 +5,26 @@ * @param {{ core: import('@actions/core'), context: import('@actions/github').Context }} params * @param {{ minOverviewLength?: number }} options */ +/** + * Revert PRs are exempt from description validation. Detection lives here rather than + * in a workflow `if` condition so the job still completes successfully instead of + * showing as skipped, which can block merges when this check is required. + */ +function isRevertPr(pr) { + const title = pr?.title ?? ''; + const headRef = pr?.head?.ref ?? ''; + return /^Revert\s/i.test(title) || /^revert[-_]/i.test(headRef); +} + module.exports = async function validatePrDescription({ core, context }, options = {}) { const minOverviewLength = Number(options.minOverviewLength) || 40; const pr = context.payload.pull_request; + + if (isRevertPr(pr)) { + core.info('Skipping PR description check for revert PR.'); + return; + } + const body = pr?.body ?? ''; if (!body.trim()) { @@ -15,7 +32,7 @@ module.exports = async function validatePrDescription({ core, context }, options return; } - if (/\*\*REPLACE ME\*\*/i.test(body)) { + if (/\*\*REPLACE ME\*\*/.test(body)) { core.setFailed('PR description still contains the "REPLACE ME" placeholder. Please fill in the overview.'); return; } diff --git a/.github/workflows/callable.pr-description-check.yaml b/.github/workflows/callable.pr-description-check.yaml index 355eea7..142c579 100644 --- a/.github/workflows/callable.pr-description-check.yaml +++ b/.github/workflows/callable.pr-description-check.yaml @@ -37,6 +37,7 @@ jobs: const script = require('${{ github.workspace }}/.github/scripts/validate-pr-description.js'); // github, context, and core are injected by github-script from this // job's runtime context (the caller's workflow run / PR payload). + // Revert PRs (title "Revert ..." or revert-* branch) are exempt — see validate-pr-description.js. await script( { github, context, core }, { minOverviewLength: Number(process.env.MIN_OVERVIEW_LENGTH) || 40 } From 5ab2b7cdcc5285bd4c87b819dfc36200944c5438 Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Wed, 26 Aug 2026 13:53:06 -0400 Subject: [PATCH 3/4] refactor: tighten PR description check workflow triggers Clarify Overview template comment, rely on script default for min length, and run the local check only on opened, edited, and reopened PR events. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/callable.pr-description-check.yaml | 2 +- .github/workflows/local.pr-description-check.yaml | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 02a9507..7864c9b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ ## ℹ️ Overview - + **REPLACE ME**: Provide the context and description of the change. diff --git a/.github/workflows/callable.pr-description-check.yaml b/.github/workflows/callable.pr-description-check.yaml index 142c579..c443e37 100644 --- a/.github/workflows/callable.pr-description-check.yaml +++ b/.github/workflows/callable.pr-description-check.yaml @@ -40,5 +40,5 @@ jobs: // Revert PRs (title "Revert ..." or revert-* branch) are exempt — see validate-pr-description.js. await script( { github, context, core }, - { minOverviewLength: Number(process.env.MIN_OVERVIEW_LENGTH) || 40 } + { minOverviewLength: Number(process.env.MIN_OVERVIEW_LENGTH) } ); diff --git a/.github/workflows/local.pr-description-check.yaml b/.github/workflows/local.pr-description-check.yaml index 12ab458..87fd19f 100644 --- a/.github/workflows/local.pr-description-check.yaml +++ b/.github/workflows/local.pr-description-check.yaml @@ -5,8 +5,6 @@ on: - opened - edited - reopened - - labeled - - unlabeled jobs: From ab23e8424907d6833af09720b530c5470aebeea7 Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Wed, 26 Aug 2026 13:58:40 -0400 Subject: [PATCH 4/4] chore: remove space --- .github/workflows/local.pr-description-check.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/local.pr-description-check.yaml b/.github/workflows/local.pr-description-check.yaml index 87fd19f..3a49f5f 100644 --- a/.github/workflows/local.pr-description-check.yaml +++ b/.github/workflows/local.pr-description-check.yaml @@ -6,7 +6,6 @@ on: - edited - reopened - jobs: call-pr-description-check-workflow: uses: ./.github/workflows/callable.pr-description-check.yaml