diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml
index 6691fec3341c9d..7a1fa210d091af 100644
--- a/.github/workflows/pull-request-automation.yml
+++ b/.github/workflows/pull-request-automation.yml
@@ -2,12 +2,15 @@ on:
pull_request_target:
types: [opened]
push:
+ workflow_run:
+ workflows: [Build Gutenberg Plugin Zip]
+ types: [completed, in_progress]
name: Pull request automation
jobs:
pull-request-automation:
runs-on: ubuntu-latest
- if: ${{ github.repository == 'WordPress/gutenberg' }}
+ if: ${{ true || github.repository == 'WordPress/gutenberg' }}
strategy:
matrix:
node: ['16']
diff --git a/changelog.txt b/changelog.txt
index 6d47c988544586..8844f19d0c62d5 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -224,6 +224,7 @@
### Tools
- Try: Change PR label enforcer automation not to work on draft PRs by default. ([53417](https://github.com/WordPress/gutenberg/pull/53417))
+- Insert build status comment on new PRs. ([54303](https://github.com/WordPress/gutenberg/pull/54303))
#### Testing
- Attempt to fix intermittent end-to-end test failure. ([53905](https://github.com/WordPress/gutenberg/pull/53905))
diff --git a/packages/project-management-automation/README.md b/packages/project-management-automation/README.md
index 3f2b4383c3a56e..171c9d28bf1b52 100644
--- a/packages/project-management-automation/README.md
+++ b/packages/project-management-automation/README.md
@@ -5,6 +5,7 @@ This is a [GitHub Action](https://help.github.com/en/categories/automating-your-
- [First Time Contributor](https://github.com/WordPress/gutenberg/tree/HEAD/packages/project-management-automation/lib/tasks/first-time-contributor): Adds the "First Time Contributor" label to pull requests merged on behalf of contributors that have not previously made a contribution, and prompts the user to link their GitHub account to their WordPress.org profile if necessary for release notes credit.
- [Add Milestone](https://github.com/WordPress/gutenberg/tree/HEAD/packages/project-management-automation/lib/tasks/add-milestone): Assigns the plugin release milestone to a pull request once it is merged.
- [Assign Fixed Issues](https://github.com/WordPress/gutenberg/tree/HEAD/packages/project-management-automation/lib/tasks/assign-fixed-issues): Adds assignee for issues which are marked to be "Fixed" by a pull request, and adds the "In Progress" label.
+- [PR Preview Link](https://github.com/WordPress/gutenberg/tree/HEAD/packages/project-management-automation/lib/tasks/pr-preview-link): Adds a comment to new PRs with a link to `http://gutenberg.run/{pr_number}` and Gutenberg plugin build file URL, making it easier for non-tech contributors to test.
# Installation and usage
@@ -37,4 +38,4 @@ This is an individual package that's part of the Gutenberg project. The project
To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md).
-

+

\ No newline at end of file
diff --git a/packages/project-management-automation/lib/index.js b/packages/project-management-automation/lib/index.js
index b4f72a51efec6e..e9b8d13f7410cc 100644
--- a/packages/project-management-automation/lib/index.js
+++ b/packages/project-management-automation/lib/index.js
@@ -11,6 +11,7 @@ const assignFixedIssues = require( './tasks/assign-fixed-issues' );
const firstTimeContributorAccountLink = require( './tasks/first-time-contributor-account-link' );
const firstTimeContributorLabel = require( './tasks/first-time-contributor-label' );
const addMilestone = require( './tasks/add-milestone' );
+const prPreviewLink = require( './tasks/pr-preview-link' );
const debug = require( './debug' );
/**
@@ -52,6 +53,10 @@ const automations = [
event: 'push',
task: addMilestone,
},
+ {
+ event: 'workflow_run',
+ task: prPreviewLink,
+ },
];
( async function main() {
diff --git a/packages/project-management-automation/lib/tasks/pr-preview-link/README.md b/packages/project-management-automation/lib/tasks/pr-preview-link/README.md
new file mode 100644
index 00000000000000..6cce7ac128b633
--- /dev/null
+++ b/packages/project-management-automation/lib/tasks/pr-preview-link/README.md
@@ -0,0 +1,12 @@
+PR Preview Link
+===
+
+Adds a comment to new PRs with a comment about Gutenberg plugin build status:
+- Latest commit
+- Build status of [Build Gutenberg Plugin Zip](https://github.com/WordPress/gutenberg/blob/d19eb92d96886f4bb8e1028c5d54d365e37d71e4/.github/workflows/build-plugin-zip.yml#L1)
+- Link to live preview `http://gutenberg.run/{pr_number}`
+- Link to artifact download URL
+
+## Rationale
+
+Preview sites make PRs much easier to test, especially for folks who don't have a dev environment setup. The comment also saves contributors some extra clicks to download the latest build of the Gutenberg plugin.
\ No newline at end of file
diff --git a/packages/project-management-automation/lib/tasks/pr-preview-link/index.js b/packages/project-management-automation/lib/tasks/pr-preview-link/index.js
new file mode 100644
index 00000000000000..8fab5ccb471377
--- /dev/null
+++ b/packages/project-management-automation/lib/tasks/pr-preview-link/index.js
@@ -0,0 +1,174 @@
+// @ts-nocheck
+
+/**
+ * Internal dependencies
+ */
+const debug = require( '../../debug' );
+
+/** @typedef {ReturnType} GitHub */
+/** @typedef {import('@octokit/webhooks-types').WorkflowRunEvent} WorkflowRunEvent */
+
+/**
+ * Identifier used to find existing comment.
+ *
+ * @type {string}
+ */
+const COMMENT_PLACEHOLDER = 'gutenberg-run-placeholder:cmt@v1';
+
+const createBuildSummary = async (
+ { buildStatus, latestCommit, pullRequestNumber, artifact },
+ octokit
+) => {
+ let status, previewMsg, artifactMsg;
+ status = previewMsg = artifactMsg = 'Building in progress...';
+ if ( buildStatus === 'success' ) {
+ status = 'Build successful!';
+ previewMsg = `[gutenberg.run/${ pullRequestNumber }](gutenberg.run/${ pullRequestNumber } )`;
+ artifactMsg = `[gutenberg-plugin](${ artifact.url }) - ${ artifact.size } MB`;
+ } else if ( buildStatus === 'failure' ) {
+ status = previewMsg = artifactMsg = 'Build failed!';
+ }
+
+ const response = await octokit.rest.markdown.render( {
+ mode: 'gfm',
+ text: `
+# Gutenberg Plugin build status
+
+| Name | Result |
+| ----------------------- | - |
+| **Last commit:** | ${ latestCommit } |
+| **Status**: | ${ status } |
+| **Preview URL**: | ${ previewMsg } |
+| **Gutenberg plugin zip**: | ${ artifactMsg } |
+ `,
+ } );
+ return `${ response.data }`;
+};
+
+const writeComment = async (
+ { owner, repo, pullRequestNumber, commentBody },
+ octokit
+) => {
+ debug( 'pr-preview-link: Find and replace build status comment' );
+ // First check if there is already a comment from this action
+ const comments = await octokit.rest.issues.listComments( {
+ issue_number: pullRequestNumber,
+ owner,
+ repo,
+ } );
+
+ const existingComment = comments.data.filter( ( comment ) =>
+ comment.body.includes( COMMENT_PLACEHOLDER )
+ );
+
+ if ( existingComment.length ) {
+ await octokit.rest.issues.updateComment( {
+ owner,
+ repo,
+ comment_id: existingComment[ 0 ].id,
+ body: commentBody,
+ } );
+ } else {
+ await octokit.rest.issues.createComment( {
+ owner,
+ repo,
+ issue_number: pullRequestNumber,
+ body: commentBody,
+ } );
+ }
+};
+
+/**
+ * Adds a comment to new PRs with a link to the corresponding gutenberg.run preview site.
+ *
+ * @param {WorkflowRunEvent} payload Workflow Run event payload.
+ * @param {GitHub} octokit Initialized Octokit REST client.
+ */
+async function prPreviewLink( payload, octokit ) {
+ const action = payload.action;
+ const repo = payload.repository.name;
+ const owner = payload.repository.owner.login;
+ const repoHtmlUrl = payload.repository.html_url;
+ const workflowRun = payload.workflow_run;
+
+ if ( ! workflowRun || workflowRun?.event === 'workflow_dispatch' ) {
+ return;
+ }
+
+ const workflowRunId = workflowRun.id;
+ const pullRequestNumber = workflowRun.pull_requests[ 0 ].number;
+ const checkSuiteId = workflowRun.check_suite_id;
+ const latestCommit = `${ repoHtmlUrl }/pull/${ pullRequestNumber }/commits/${ workflowRun.head_sha }`;
+
+ if ( action === 'in_progress' ) {
+ const commentBody = await createBuildSummary(
+ {
+ buildStatus: action,
+ latestCommit,
+ pullRequestNumber,
+ artifact: null,
+ },
+ octokit
+ );
+
+ await writeComment(
+ { owner, repo, pullRequestNumber, commentBody },
+ octokit
+ );
+ }
+
+ if ( action === 'completed' ) {
+ debug( 'pr-preview-link: Build complete, request artifact from API' );
+ const artifactsResponse =
+ await octokit.rest.actions.listWorkflowRunArtifacts( {
+ owner,
+ repo,
+ run_id: workflowRunId,
+ name: 'gutenberg-plugin',
+ per_page: 1,
+ } );
+ const artifacts = artifactsResponse.data.artifacts;
+
+ let commentBody;
+ if ( ! artifacts.length ) {
+ debug( 'pr-preview-link: No artifact found, mark as failure' );
+
+ commentBody = await createBuildSummary(
+ {
+ buildStatus: 'failure',
+ latestCommit,
+ pullRequestNumber,
+ artifactsUrl: null,
+ },
+ octokit
+ );
+ } else {
+ debug( 'pr-preview-link: Found artifact, mark as success' );
+
+ const artifact = artifacts[ 0 ];
+ // The artifact URL on Checks screen
+ const artifactUrl = `${ repoHtmlUrl }/suites/${ checkSuiteId }/artifacts/${ artifact.id }`;
+ const sizeInMB = artifact.size_in_bytes / ( 1024 * 1024 );
+ commentBody = await createBuildSummary(
+ {
+ buildStatus: 'success',
+ latestCommit,
+ pullRequestNumber,
+ artifact: {
+ url: artifactUrl,
+ size: sizeInMB.toFixed( 2 ),
+ },
+ },
+ octokit
+ );
+ }
+
+ await writeComment(
+ { owner, repo, pullRequestNumber, commentBody },
+ octokit
+ );
+ }
+}
+
+module.exports = prPreviewLink;
+module.exports.COMMENT_PLACEHOLDER = COMMENT_PLACEHOLDER;
diff --git a/packages/project-management-automation/lib/tasks/pr-preview-link/test/index.js b/packages/project-management-automation/lib/tasks/pr-preview-link/test/index.js
new file mode 100644
index 00000000000000..a695d83a5ff1fd
--- /dev/null
+++ b/packages/project-management-automation/lib/tasks/pr-preview-link/test/index.js
@@ -0,0 +1,310 @@
+/**
+ * Internal dependencies
+ */
+import prPreviewLink, { COMMENT_PLACEHOLDER } from '../';
+
+describe( 'prPreviewLink', () => {
+ it( 'does nothing if event is not workflow_run', async () => {
+ const payload = {
+ workflow_run: null,
+ repository: {
+ name: 'gutenberg',
+ owner: 'WordPress',
+ },
+ };
+ const octokit = {
+ rest: {
+ actions: {
+ listWorkflowRunArtifacts: jest.fn(),
+ },
+ issues: {
+ listComments: jest.fn(),
+ updateComment: jest.fn(),
+ createComment: jest.fn(),
+ },
+ markdown: {
+ render: jest.fn(),
+ },
+ },
+ };
+
+ await prPreviewLink( payload, octokit );
+
+ expect( octokit.rest.markdown.render ).not.toHaveBeenCalled();
+ expect( octokit.rest.issues.listComments ).not.toHaveBeenCalled();
+ expect( octokit.rest.issues.updateComment ).not.toHaveBeenCalled();
+ expect( octokit.rest.issues.createComment ).not.toHaveBeenCalled();
+ expect(
+ octokit.rest.actions.listWorkflowRunArtifacts
+ ).not.toHaveBeenCalled();
+ } );
+
+ it( 'does nothing if action is neither in_progress nor completed', async () => {
+ const payload = {
+ action: 'requested',
+ repository: {
+ name: 'gutenberg',
+ owner: {
+ login: 'WordPress',
+ },
+ html_url: 'https://github.com/WordPress/gutenberg',
+ },
+ workflow_run: {
+ event: 'workflow_run',
+ id: 123,
+ pull_requests: [
+ {
+ number: 456,
+ },
+ ],
+ head_sha: 'abcdef12345',
+ },
+ };
+
+ // Mock Octokit methods
+ const octokit = {
+ rest: {
+ markdown: {
+ render: jest.fn(),
+ },
+ },
+ };
+
+ // Call prPreviewLink with the mock payload and octokit
+ await prPreviewLink( payload, octokit );
+
+ // Expect the markdown.render method not to be called
+ expect( octokit.rest.markdown.render ).not.toHaveBeenCalled();
+ } );
+
+ it( 'renders the correct Markdown content for an in-progress build', async () => {
+ const payload = {
+ action: 'in_progress',
+ repository: {
+ name: 'gutenberg',
+ owner: {
+ login: 'WordPress',
+ },
+ html_url: 'https://github.com/WordPress/gutenberg',
+ },
+ workflow_run: {
+ event: 'workflow_run',
+ id: 123,
+ pull_requests: [
+ {
+ number: 456,
+ },
+ ],
+ head_sha: 'abcdef12345',
+ },
+ };
+
+ // Mock Octokit methods
+ const octokit = {
+ rest: {
+ markdown: {
+ render: jest.fn( () =>
+ Promise.resolve( { data: 'Mocked Markdown Output' } )
+ ),
+ },
+ issues: {
+ listComments: jest.fn( () =>
+ Promise.resolve( { data: [] } )
+ ),
+ updateComment: jest.fn(),
+ createComment: jest.fn(),
+ },
+ },
+ };
+
+ // Call prPreviewLink with the mock payload and octokit
+ await prPreviewLink( payload, octokit );
+
+ expect( octokit.rest.markdown.render ).toHaveBeenCalledWith( {
+ mode: 'gfm',
+ text: expect.stringContaining( 'Building in progress...' ),
+ } );
+ expect( octokit.rest.issues.listComments ).toHaveBeenCalledWith( {
+ issue_number: 456,
+ owner: 'WordPress',
+ repo: 'gutenberg',
+ } );
+ expect( octokit.rest.issues.createComment ).toHaveBeenCalledWith( {
+ issue_number: 456,
+ owner: 'WordPress',
+ repo: 'gutenberg',
+ body: `Mocked Markdown Output`,
+ } );
+ expect( octokit.rest.issues.updateComment ).not.toHaveBeenCalled();
+ } );
+
+ it( 'adds a comment on successful build', async () => {
+ const payload = {
+ action: 'completed',
+ repository: {
+ name: 'gutenberg',
+ owner: {
+ login: 'WordPress',
+ },
+ html_url: 'https://github.com/WordPress/gutenberg',
+ },
+ workflow_run: {
+ event: 'workflow_run',
+ id: 123,
+ pull_requests: [
+ {
+ number: 456,
+ },
+ ],
+ check_suite_id: 789,
+ head_sha: 'abcdef12345',
+ },
+ };
+
+ // Mock Octokit methods
+ const octokit = {
+ rest: {
+ actions: {
+ listWorkflowRunArtifacts: jest.fn( () => {
+ return Promise.resolve( {
+ data: {
+ artifacts: [
+ {
+ id: '987',
+ size_in_bytes: 1024 * 1024, // 1 MB
+ },
+ ],
+ },
+ } );
+ } ),
+ },
+ issues: {
+ listComments: jest.fn( () => {
+ // Mock an existing comment from the same action
+ return Promise.resolve( {
+ data: [
+ {
+ id: 789,
+ body: `Mocked existing comment`,
+ },
+ ],
+ } );
+ } ),
+ updateComment: jest.fn(),
+ createComment: jest.fn(),
+ },
+ markdown: {
+ render: jest.fn( () => {
+ return Promise.resolve( {
+ data: 'Mocked rendered comment',
+ } );
+ } ),
+ },
+ },
+ };
+
+ // Call prPreviewLink with the mock payload and octokit
+ await prPreviewLink( payload, octokit );
+
+ expect( octokit.rest.markdown.render ).toHaveBeenCalledWith( {
+ mode: 'gfm',
+ text: expect.stringContaining( 'Build successful!' ),
+ } );
+ // Expect markdown render to contain the artifact URL
+ expect( octokit.rest.markdown.render ).toHaveBeenCalledWith( {
+ mode: 'gfm',
+ text: expect.stringContaining(
+ '[gutenberg-plugin](https://github.com/WordPress/gutenberg/suites/789/artifacts/987) - 1.00 MB'
+ ),
+ } );
+ // Expect the existing comment to be updated
+ expect( octokit.rest.issues.updateComment ).toHaveBeenCalledWith( {
+ owner: 'WordPress',
+ repo: 'gutenberg',
+ comment_id: 789, // Existing comment ID
+ body: `Mocked rendered comment`,
+ } );
+ expect( octokit.rest.issues.createComment ).not.toHaveBeenCalled();
+ expect(
+ octokit.rest.actions.listWorkflowRunArtifacts
+ ).toHaveBeenCalled();
+ } );
+
+ it( 'adds a comment on failed build', async () => {
+ const payload = {
+ action: 'completed',
+ repository: {
+ name: 'gutenberg',
+ owner: {
+ login: 'WordPress',
+ },
+ html_url: 'https://github.com/WordPress/gutenberg',
+ },
+ workflow_run: {
+ event: 'workflow_run',
+ id: 123,
+ pull_requests: [
+ {
+ number: 456,
+ },
+ ],
+ head_sha: 'abcdef12345',
+ },
+ };
+
+ // Mock Octokit methods
+ const octokit = {
+ rest: {
+ actions: {
+ listWorkflowRunArtifacts: jest.fn( () => {
+ return Promise.resolve( {
+ data: {
+ artifacts: [], // Simulate no artifacts found for a failed build
+ },
+ } );
+ } ),
+ },
+ issues: {
+ listComments: jest.fn( () => {
+ // Mock an existing comment from the same action
+ return Promise.resolve( {
+ data: [
+ {
+ id: 789,
+ body: `Mocked existing comment`,
+ },
+ ],
+ } );
+ } ),
+ updateComment: jest.fn(),
+ createComment: jest.fn(),
+ },
+ markdown: {
+ render: jest.fn( () => {
+ return Promise.resolve( {
+ data: 'Mocked failed build comment',
+ } );
+ } ),
+ },
+ },
+ };
+
+ // Call prPreviewLink with the mock payload and octokit
+ await prPreviewLink( payload, octokit );
+
+ expect( octokit.rest.markdown.render ).toHaveBeenCalledWith( {
+ mode: 'gfm',
+ text: expect.stringContaining( 'Build failed!' ),
+ } );
+ expect( octokit.rest.issues.updateComment ).toHaveBeenCalledWith( {
+ owner: 'WordPress',
+ repo: 'gutenberg',
+ comment_id: 789, // Existing comment ID
+ body: `Mocked failed build comment`,
+ } );
+ expect( octokit.rest.issues.createComment ).not.toHaveBeenCalled();
+ expect(
+ octokit.rest.actions.listWorkflowRunArtifacts
+ ).toHaveBeenCalled();
+ } );
+} );