Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions .github/actions/diff-js-api-changes/action.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
name: diff-js-api-changes
description: Check for breaking changes in the public React Native JS API
outputs:
message:
description: "Formatted message for PR comment, empty if no API changes"
value: ${{ steps.format.outputs.message }}
runs:
using: composite
steps:
- name: Compute merge base with main
- name: Fetch PR and main, compute merge base
id: merge_base
shell: bash
run: |
git fetch origin main
git fetch --deepen=500
echo "merge_base=$(git merge-base HEAD origin/main)" >> $GITHUB_OUTPUT
git fetch origin ${{ github.event.pull_request.head.sha }} --depth=500
echo "merge_base=$(git merge-base ${{ github.event.pull_request.head.sha }} origin/main)" >> $GITHUB_OUTPUT

- name: Output snapshot before state for comparison
- name: Extract before and after API snapshots
shell: bash
env:
SCRATCH_DIR: ${{ runner.temp }}/diff-js-api-changes
run: |
rm -rf $SCRATCH_DIR
mkdir -p $SCRATCH_DIR
git show ${{ steps.merge_base.outputs.merge_base }}:packages/react-native/ReactNativeApi.d.ts > $SCRATCH_DIR/ReactNativeApi-before.d.ts \
|| echo "" > $SCRATCH_DIR/ReactNativeApi-before.d.ts
git show ${{ github.event.pull_request.head.sha }}:packages/react-native/ReactNativeApi.d.ts > $SCRATCH_DIR/ReactNativeApi-after.d.ts \
|| echo "" > $SCRATCH_DIR/ReactNativeApi-after.d.ts

- name: Run breaking change detection
shell: bash
Expand All @@ -27,5 +34,37 @@ runs:
run: |
node ./scripts/js-api/diff-api-snapshot \
$SCRATCH_DIR/ReactNativeApi-before.d.ts \
./packages/react-native/ReactNativeApi.d.ts \
$SCRATCH_DIR/ReactNativeApi-after.d.ts \
> $SCRATCH_DIR/output.json

- name: Format output message
id: format
shell: bash
env:
SCRATCH_DIR: ${{ runner.temp }}/diff-js-api-changes
run: |
if [ ! -f "$SCRATCH_DIR/output.json" ]; then
echo "message=" >> $GITHUB_OUTPUT
exit 0
fi

RESULT=$(cat $SCRATCH_DIR/output.json | jq -r '.result // empty')
if [ -z "$RESULT" ]; then
echo "message=" >> $GITHUB_OUTPUT
exit 0
fi

# Use delimiter for multiline output
{
echo "message<<EOF"
echo "> [!WARNING]"
echo "> **JavaScript API change detected**"
echo ">"
echo "> This PR commits an update to \`ReactNativeApi.d.ts\`, indicating a change to React Native's public JavaScript API."
echo ">"
echo "> - Please include a **clear changelog message**."
echo "> - This change will be subject to additional review."
echo ">"
echo "> This change was flagged as: \`${RESULT}\`"
echo "EOF"
} >> $GITHUB_OUTPUT
128 changes: 128 additions & 0 deletions .github/workflow-scripts/__tests__/postPRComment-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* 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
*/

const postPRComment = require('../postPRComment');
const {_COMMENT_MARKER} = postPRComment;

describe('postPRComment', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
});

const mockGithub = {
rest: {
issues: {
listComments: jest.fn(),
createComment: jest.fn(),
updateComment: jest.fn(),
deleteComment: jest.fn(),
},
},
};

const mockContext = {
repo: {owner: 'facebook', repo: 'react-native'},
payload: {pull_request: {number: 123}},
};

beforeEach(() => {
mockGithub.rest.issues.listComments.mockResolvedValue({data: []});
mockGithub.rest.issues.createComment.mockResolvedValue({});
mockGithub.rest.issues.updateComment.mockResolvedValue({});
mockGithub.rest.issues.deleteComment.mockResolvedValue({});
});

it('does nothing when no messages and no existing comment', async () => {
await postPRComment(mockGithub, mockContext, {messages: []});

expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled();
expect(mockGithub.rest.issues.deleteComment).not.toHaveBeenCalled();
});

it('filters out empty and null messages', async () => {
await postPRComment(mockGithub, mockContext, {
messages: ['', null, ' ', undefined],
});

expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled();
});

it('deletes existing comment when no messages to report', async () => {
const existingComment = {
id: 456,
body: `${_COMMENT_MARKER}\nOld content`,
};
mockGithub.rest.issues.listComments.mockResolvedValue({
data: [existingComment],
});

await postPRComment(mockGithub, mockContext, {messages: []});

expect(mockGithub.rest.issues.deleteComment).toHaveBeenCalledWith({
owner: 'facebook',
repo: 'react-native',
comment_id: 456,
});
});

it('creates new comment when there are messages', async () => {
await postPRComment(mockGithub, mockContext, {
messages: ['### Test Message\n\nSome content'],
});

expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({
owner: 'facebook',
repo: 'react-native',
issue_number: 123,
body: expect.stringContaining(_COMMENT_MARKER),
});
expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({
owner: 'facebook',
repo: 'react-native',
issue_number: 123,
body: expect.stringContaining('Test Message'),
});
});

it('updates existing comment when there are messages', async () => {
const existingComment = {
id: 789,
body: `${_COMMENT_MARKER}\nOld content`,
};
mockGithub.rest.issues.listComments.mockResolvedValue({
data: [existingComment],
});

await postPRComment(mockGithub, mockContext, {
messages: ['### Updated Message'],
});

expect(mockGithub.rest.issues.updateComment).toHaveBeenCalledWith({
owner: 'facebook',
repo: 'react-native',
comment_id: 789,
body: expect.stringContaining(_COMMENT_MARKER),
});
expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled();
});

it('combines multiple messages with double newlines', async () => {
await postPRComment(mockGithub, mockContext, {
messages: ['Message 1', 'Message 2'],
});

expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({
owner: 'facebook',
repo: 'react-native',
issue_number: 123,
body: expect.stringContaining('Message 1\n\nMessage 2'),
});
});
});
77 changes: 77 additions & 0 deletions .github/workflow-scripts/postPRComment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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
*/

const COMMENT_MARKER = '<!-- react-native-bot -->';

/**
* Posts a PR validation comment, updates an existing one, or deletes it if there's nothing to report.
*
* @param {Object} github - The octokit client from actions/github-script
* @param {Object} context - The GitHub Actions context
* @param {Object} options - Options for the comment
* @param {string[]} [options.messages] - Array of message strings to include in the comment
*/
async function postPRComment(github, context, options) {
const {owner, repo} = context.repo;
const prNumber = context.payload.pull_request.number;

const sections = (options.messages || []).filter(
msg => msg != null && msg.trim() !== '',
);

const {data: comments} = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
});

const existingComment = comments.find(comment =>
comment.body.includes(COMMENT_MARKER),
);

if (sections.length === 0) {
console.log('No issues to report');
if (existingComment) {
console.log(`Deleting existing comment ${existingComment.id}`);
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existingComment.id,
});
}
return;
}

const commentBody = `${COMMENT_MARKER}
## PR Validation

${sections.join('\n\n')}`;

if (existingComment) {
console.log(`Updating existing comment ${existingComment.id}`);
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: commentBody,
});
} else {
console.log('Creating new comment');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody,
});
}
}

module.exports = postPRComment;
// Exported for testing purposes
module.exports._COMMENT_MARKER = COMMENT_MARKER;
41 changes: 41 additions & 0 deletions .github/workflows/annotate-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Annotate PR

on:
pull_request_target:
types: [opened, edited, reopened, synchronize]

permissions:
pull-requests: write

jobs:
annotate-pr:
runs-on: ubuntu-latest
# if: github.repository == 'facebook/react-native' temp
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: Run diff-js-api-changes
id: diff-js-api
uses: ./.github/actions/diff-js-api-changes
# - name: Check PR body
# todo
# uses: ./.github/actions/check-pr-body
# - name: Branch Targetting
# todo
# uses: ./.github/actions/check-branch-target
- name: Post PR comment
uses: actions/github-script@v8
env:
API_CHANGES_MESSAGE: ${{ steps.diff-js-api.outputs.message }}
with:
script: |
const postPRComment = require('./.github/workflow-scripts/postPRComment.js');
await postPRComment(github, context, {
messages: [
process.env.API_CHANGES_MESSAGE,
],
});
15 changes: 9 additions & 6 deletions .github/workflows/danger-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:
jobs:
danger:
runs-on: ubuntu-latest
if: github.repository == 'facebook/react-native'
# if: github.repository == 'facebook/react-native'
steps:
- name: Check out PR branch
uses: actions/checkout@v6
Expand All @@ -25,8 +25,11 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Run diff-js-api-changes
uses: ./.github/actions/diff-js-api-changes
- name: Danger
run: yarn danger ci --use-github-checks --failOnErrors
working-directory: private/react-native-bots
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
- name: Debug - Show API diff output
run: cat ${{ runner.temp }}/diff-js-api-changes/output.json
# Danger step removed for fork testing
# - name: Danger
# run: yarn danger ci --use-github-checks --failOnErrors
# working-directory: private/react-native-bots
# env:
# DANGER_GITHUB_API_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
Loading