Add PR body validation to analyze-pr workflow (#55382)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/55382

This adds a `validatePRBody.js` script that checks pull request descriptions for required sections as part of a new `analyze-pr.yml` workflow that will replace part of the Danger-pr workflow.

The validation includes:
- **Description length check**: Fails if the PR body is missing or less than 50 characters
- **Summary section check**: Warns if the PR lacks a "## Summary" section
- **Test plan check**: Warns if the PR lacks a "## Test Plan" section
- **Changelog validation**: failing if missing or invalid

Key behaviors:
- Phabricator-sourced PRs (detected via "Differential Revision:" in body) skip summary, test plan, and changelog validation since these are enforced differently
- Validation messages use GitHub's `[!WARNING]` and `[!CAUTION]` callout syntax for clear visual feedback
- The workflow fails (via `core.setFailed`) when any required check fails
- Messages are passed to `postPRComment` alongside API changes for consolidated PR comments

This aims to mimic relevant parts of dangerfile.js

Changelog: [Internal]

Reviewed By: huntie

Differential Revision: D91158803

fbshipit-source-id: 2304251d18f9fc8bf9a29e536fff2d979573bd86
This commit is contained in:
Emily Brown
2026-02-06 08:14:53 -08:00
committed by meta-codesync[bot]
parent 6a9d792f9a
commit 5e41043fb5
2 changed files with 139 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
'use strict';
const changelogRegex =
/\[\s?(ANDROID|GENERAL|IOS|INTERNAL)\s?\]\s?\[\s?(BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY)\s?\]\s*?-?\s*?(.*)/i;
const internalChangelogRegex = /\[\s?(INTERNAL)\s?\].*/gi;
function validateChangelog(commitMsg) {
if (!commitMsg.toLowerCase().includes('changelog:')) {
return 'missing';
}
const hasValidChangelog = changelogRegex.test(commitMsg);
const hasValidInternalChangelog = internalChangelogRegex.test(commitMsg);
if (hasValidChangelog || hasValidInternalChangelog) {
return 'valid';
}
return 'invalid';
}
function validatePRBody(prBody) {
const body = prBody ?? '';
const bodyLower = body.toLowerCase();
const isFromPhabricator = bodyLower.includes('differential revision:');
const messages = [];
let hasFail = false;
function addWarning(title, text) {
messages.push(`> [!WARNING]
> **${title}**
>
> ${text}`);
}
function addFail(title, text) {
hasFail = true;
messages.push(`> [!CAUTION]
> **${title}**
>
> ${text}`);
}
if (!body || body.length < 50) {
addFail('Missing Description', 'This pull request needs a description.');
} else {
const hasSummary =
bodyLower.includes('## summary') || bodyLower.includes('summary:');
if (!hasSummary && body.split('\n').length <= 2 && !isFromPhabricator) {
addWarning(
'Missing Summary',
'Please add a "## Summary" section to your PR description. This is a good place to explain the motivation for making this change.',
);
}
}
if (!isFromPhabricator) {
const hasTestPlan = ['## test plan', 'test plan:', 'tests:', 'test:'].some(
t => bodyLower.includes(t),
);
if (!hasTestPlan) {
addWarning(
'Missing Test Plan',
'Please add a "## Test Plan" section to your PR description. A Test Plan lets us know how these changes were tested.',
);
}
}
if (!isFromPhabricator) {
const status = validateChangelog(body);
const link =
'https://reactnative.dev/contributing/changelogs-in-pull-requests';
if (status === 'missing') {
addFail(
'Missing Changelog',
`Please add a Changelog to your PR description. See [Changelog format](${link})`,
);
} else if (status === 'invalid') {
addFail(
'Invalid Changelog Format',
`Please verify your Changelog format. See [Changelog format](${link})`,
);
}
}
return {
message: messages.join('\n\n'),
status: hasFail ? 'FAIL' : 'PASS',
};
}
module.exports = validatePRBody;
+37
View File
@@ -0,0 +1,37 @@
name: Analyze Pull Request
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
permissions:
pull-requests: write
jobs:
analyze-pr:
runs-on: ubuntu-latest
if: github.repository == 'facebook/react-native'
steps:
- name: Check out main branch
uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Run yarn install
uses: ./.github/actions/yarn-install
- name: Check PR body
id: check-pr-body
uses: actions/github-script@v8
with:
script: |
const validatePRBody = require('./.github/workflow-scripts/validatePRBody.js');
const {message, status} = validatePRBody(context.payload.pull_request.body);
core.setOutput('message', message);
core.setOutput('status', status);
- name: Post PR comment
uses: ./.github/actions/post-pr-comment
with:
marker: '<!-- analyze-pr -->'
sections: '[${{ toJSON(steps.check-pr-body.outputs.message) }}]'
- name: Fail if validation errors
if: steps.check-pr-body.outputs.status == 'FAIL'
run: exit 1