name: Cherry Pick Tool on: issues: types: [milestoned, labeled] pull_request: types: [closed] workflow_dispatch: inputs: release_branch: description: Provide the release branch you want to cherry pick into. Example release/6.9 default: '' required: true pull_requests: description: The pull request number. default: '' required: true skipSlackPing: description: "Skip Slack Ping: If true, the Slack ping will be skipped (useful for testing)" type: boolean required: false default: false slackChannelOverride: description: "Slack Channel Override: The channel ID to send the Slack ping about the code freeze violation" required: false default: '' env: GIT_COMMITTER_NAME: 'WooCommerce Bot' GIT_COMMITTER_EMAIL: 'no-reply@woocommerce.com' GIT_AUTHOR_NAME: 'WooCommerce Bot' GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' jobs: verify: name: Verify runs-on: ubuntu-20.04 outputs: run: ${{ steps.check.outputs.run }} steps: - name: check id: check uses: actions/github-script@v6 with: script: | let run = false; const isManualTrigger = context.payload.inputs && context.payload.inputs.release_branch && context.payload.inputs.release_branch != null; const isMergedMilestonedIssue = context.payload.issue && context.payload.issue.pull_request != null && context.payload.issue.pull_request.merged_at != null && context.payload.issue.milestone != null; const isMergedMilestonedPR = context.payload.pull_request && context.payload.pull_request != null && context.payload.pull_request.merged == true && context.payload.pull_request.milestone != null; const isBot = context.payload.pull_request && ( context.payload.pull_request.user.login == 'github-actions[bot]' || context.payload.pull_request.user.type == 'Bot' ); if ( !isBot && ( isManualTrigger || isMergedMilestonedIssue || isMergedMilestonedPR ) ) { console.log( "::set-output name=run::true" ); } else { console.log( "::set-output name=run::false" ); } prep: name: Prep inputs runs-on: ubuntu-20.04 needs: verify if: needs.verify.outputs.run == 'true' outputs: release: ${{ steps.prep-inputs.outputs.release }} pr: ${{ steps.prep-inputs.outputs.pr }} version: ${{ steps.prep-inputs.outputs.version }} steps: - name: Prep inputs id: prep-inputs uses: actions/github-script@v6 with: script: | const event = ${{ toJSON( github.event ) }} // Means this workflow was triggered manually. if ( event.inputs && event.inputs.release_branch ) { const releaseBranch = '${{ inputs.release_branch }}' const version = releaseBranch.replace( 'release/', '' ); console.log( "::set-output name=version::" + version ) console.log( "::set-output name=release::${{ inputs.release_branch }}" ) } else if ( event.action === 'milestoned' ) { const version = '${{ github.event.issue.milestone.title }}' const release = version.substring( 0, 3 ) console.log( "::set-output name=version::" + version ) console.log( "::set-output name=release::release/" + release ) } else { const version = '${{ github.event.pull_request.milestone.title }}' const release = version.substring( 0, 3 ) console.log( "::set-output name=version::" + version ) console.log( "::set-output name=release::release/" + release ) } // Means this workflow was triggered manually. if ( event.inputs && event.inputs.pull_requests ) { console.log( "::set-output name=pr::${{ inputs.pull_requests }}" ) } else if ( event.action === 'milestoned' ) { console.log( "::set-output name=pr::${{ github.event.issue.number }}" ) } else { console.log( "::set-output name=pr::${{ github.event.pull_request.number }}" ) } check-release-branch-exists: name: Check for existence of release branch runs-on: ubuntu-20.04 needs: prep steps: - name: Check for release branch id: release-breanch-check uses: actions/github-script@v6 with: script: | // This will throw an error for non-200 responses, which prevents subsequent jobs from completing, as desired. await github.request( 'GET /repos/{owner}/{repo}/branches/{branch}', { owner: context.repo.owner, repo: context.repo.repo, branch: '${{ needs.prep.outputs.release }}', } ); cherry-pick-run: name: Run cherry pick tool runs-on: ubuntu-20.04 needs: [ prep, check-release-branch-exists ] if: success() steps: - name: Checkout release branch uses: actions/checkout@v3 with: fetch-depth: 0 - name: Git fetch the release branch run: git fetch origin ${{ needs.prep.outputs.release }} - name: Checkout release branch run: git checkout ${{ needs.prep.outputs.release }} - name: Create a cherry pick branch based on release branch run: git checkout -b cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }} - name: Get commit sha from PR id: commit-sha uses: actions/github-script@v6 with: script: | const pr = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: '${{ needs.prep.outputs.pr }}' }) console.log( `::set-output name=sha::${ pr.data.merge_commit_sha }` ) - name: Cherry pick run: | git cherry-pick ${{ steps.commit-sha.outputs.sha }} - name: Generate changelog id: changelog uses: actions/github-script@v6 with: script: | const fs = require( 'node:fs' ); const changelogsToBeDeleted = [] let changelogTxt = ''; const commit = await github.rest.repos.getCommit({ owner: context.repo.owner, repo: context.repo.repo, ref: '${{ steps.commit-sha.outputs.sha }}' }) for ( const file of commit.data.files ) { if ( file.filename.match( 'plugins/woocommerce/changelog/' ) ) { if ( changelogsToBeDeleted.indexOf( file.filename ) === -1 ) { changelogsToBeDeleted.push( file.filename ); } let changelogEntry = ''; let changelogEntryType = ''; fs.readFile( './' + file.filename, 'utf-8', function( err, data ) { if ( err ) { console.error( err ); } const changelogEntryArr = data.split( "\n" ); changelogEntryType = data.match( /Type: (.+)/i ); changelogEntryType = changelogEntryType[ 1 ].charAt( 0 ).toUpperCase() + changelogEntryType[ 1 ].slice( 1 ); changelogEntry = changelogEntryArr.filter( el => { return el !== null && typeof el !== 'undefined' && el !== ''; } ); changelogEntry = changelogEntry[ changelogEntry.length - 1 ]; // Check if changelogEntry is what we want. if ( changelogEntry.length < 1 ) { changelogEntry = false; } if ( changelogEntry.match( /significance:/i ) ) { changelogEntry = false; } if ( changelogEntry.match( /type:/i ) ) { changelogEntry = false; } if ( changelogEntry.match( /comment:/i ) ) { changelogEntry = false; } if ( ! changelogEntry ) { return; } fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { if ( err ) { console.error( err ); } changelogTxt = data.split( "\n" ); let isInRange = false; let newChangelogTxt = []; for ( const line of changelogTxt ) { if ( isInRange === false && line === '== Changelog ==' ) { isInRange = true; } if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) { isInRange = false; } // Find the first match of the entry "Type". if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) { newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` ); newChangelogTxt.push( line ); isInRange = false; continue; } newChangelogTxt.push( line ); } fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => { if ( err ) { console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` ); } } ); } ); } ); } } console.log( `::set-output name=changelogsToBeDeleted::${ changelogsToBeDeleted }` ) - name: Delete changelog files from cherry pick branch if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }} - name: Commit changes for cherry pick if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git commit --no-verify -am "Prep for cherry pick ${{ needs.prep.outputs.pr }}" - name: Push cherry pick branch up run: git push origin cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }} - name: Create the PR for cherry pick branch id: cherry-pick-pr uses: actions/github-script@v6 with: script: | let cherryPickPRBody = "This PR cherry-picks the following PRs into the release branch:\n"; cherryPickPRBody = `${cherryPickPRBody}` + `* #${{ needs.prep.outputs.pr }}` + "\n"; const pr = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Cherry pick ${{ needs.prep.outputs.pr }} into ${{ needs.prep.outputs.release }}", head: "cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}", base: "${{ needs.prep.outputs.release }}", body: cherryPickPRBody }) console.log( `::set-output name=cherry-pick-pr::${ pr.data.html_url }` ) - name: Checkout trunk branch if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git checkout trunk - name: Create a branch based on trunk branch if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git checkout -b delete-changelogs/${{ needs.prep.outputs.pr }} - name: Delete changelogs from trunk if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }} - name: Commit changes for deletion if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git commit --no-verify -am "Delete changelog files for ${{ needs.prep.outputs.pr }}" - name: Push deletion branch up if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git push origin delete-changelogs/${{ needs.prep.outputs.pr }} - name: Create the PR for deletion branch id: deletion-pr if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null uses: actions/github-script@v6 with: script: | const pr = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Delete changelog files based on PR ${{ needs.prep.outputs.pr }}", head: "delete-changelogs/${{ needs.prep.outputs.pr }}", base: "trunk", body: "Delete changelog files based on PR #${{ needs.prep.outputs.pr }}" }) console.log( `::set-output name=deletion-pr::${ pr.data.html_url }` ) - name: Notify Slack on failure if: ${{ failure() && inputs.skipSlackPing != true }} uses: archive/github-actions-slack@v2.0.0 with: slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }} slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }} slack-text: | :warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube: An attempt to cherry pick PR(s) into outgoing release '${{ needs.prep.outputs.release }}' has failed. This could be due to a merge conflict or something else that requires manual attention. Please check: https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }} - name: Notify Slack on success if: ${{ success() && inputs.skipSlackPing != true }} uses: archive/github-actions-slack@v2.0.0 with: slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }} slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }} slack-text: | :warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube: Release lead please review: ${{ steps.cherry-pick-pr.outputs.cherry-pick-pr }} ${{ steps.deletion-pr.outputs.deletion-pr }}