From 90208ec288b60473257b6c62f0d2854925ad8b12 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 10 Oct 2024 15:55:56 -0300 Subject: [PATCH 1/2] Add a release creation Github action --- .gitattributes | 2 + .github/workflows/deploy.yml | 74 ++++++---- .github/workflows/release-new-version.yml | 71 ++++++++++ package.json | 26 ++++ update-version-and-changelog.js | 159 ++++++++++++++++++++++ 5 files changed, 307 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/release-new-version.yml create mode 100644 package.json create mode 100644 update-version-and-changelog.js diff --git a/.gitattributes b/.gitattributes index 02d0834f..c7288583 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,6 +10,8 @@ /.gitattributes export-ignore /composer.json export-ignore /composer.lock export-ignore +/package.json export-ignore +/update-version-and-changelog.js export-ignore # # Auto detect text files and perform LF normalization diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e60cc81a..5a688d31 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,28 +1,52 @@ name: Deploy to WordPress.org + on: - release: - types: [published] + pull_request: + types: [closed] + branches: + - trunk + jobs: - tag: - name: New release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: WordPress Plugin Deploy - id: deploy - uses: 10up/action-wordpress-plugin-deploy@stable - with: - generate-zip: true - env: - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - - name: Upload release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{github.workspace}}/${{ github.event.repository.name }}.zip - asset_name: ${{ github.event.repository.name }}.zip - asset_content_type: application/zip + deploy-to-wordpress: + if: > + github.event_name == 'pull_request' && + github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'release/') && + ( contains(github.event.pull_request.head.ref, '/major') || contains(github.event.pull_request.head.ref, '/minor') || contains(github.event.pull_request.head.ref, '/patch') ) && + ( github.event.pull_request.user.login == 'github-actions[bot]' ) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Install node dependencies + run: npm install + + - name: Get New Version + id: get-version + run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Create Tag and Release on GitHub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=v${{ steps.get-version.outputs.VERSION }} + git tag $VERSION + git push origin $VERSION + gh release create $VERSION --generate-notes + + - name: Deploy Plugin to WordPress Plugin Directory + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + VERSION: ${{ steps.get-version.outputs.VERSION }} + + - name: WordPress.org plugin asset/readme update + uses: 10up/action-wordpress-plugin-asset-update@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml new file mode 100644 index 00000000..502c0221 --- /dev/null +++ b/.github/workflows/release-new-version.yml @@ -0,0 +1,71 @@ +name: Create new release PR + +on: + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + type: choice + options: + - major + - minor + - patch + +jobs: + prepare-release: + if: github.event_name == 'workflow_dispatch' + name: Prepare Release PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install node dependencies + run: npm install + + - name: Compile Javascript App + run: npm run build + + - name: Create version update branch + id: create-branch + run: | + BRANCH_NAME="release/$(date +%Y-%m-%d)/${{ github.event.inputs.release_type }}-release" + git checkout -b $BRANCH_NAME + echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Update version and changelog + id: update-version + run: | + npm run update-version + echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + env: + RELEASE_TYPE: ${{ github.event.inputs.release_type }} + + - name: Commit changes + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Version bump & changelog update" --no-verify + git push --set-upstream origin ${{ steps.create-branch.outputs.BRANCH_NAME }} + + - name: Create Pull Request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --title "[Automation] New ${{ github.event.inputs.release_type }} Release: ${{ steps.update-version.outputs.NEW_VERSION }}" \ + --base trunk \ + --head ${{ steps.create-branch.outputs.BRANCH_NAME }} \ + --label "Release: ${{ github.event.inputs.release_type }}" \ + --body " + ### Release PR πŸ€– + This is a release PR for version **${{ steps.update-version.outputs.NEW_VERSION }}**, run by **@${{ github.actor }}**. + It updates the version of the Plugin and adds changes since the last tag to the Changelog file. + Merging this PR will trigger a new release and update the Plugin in the WordPress Plugin Directory." diff --git a/package.json b/package.json new file mode 100644 index 00000000..fb8644c3 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "theme-check", + "version": "20231220", + "description": "Create a block-based theme", + "author": "The theme check plugin is an easy way to test your theme and make sure it’s up to spec with the latest theme review standards.", + "license": "GPL-2.0-or-later", + "keywords": [ + "WordPress", + "theme" + ], + "homepage": "https://wordpress.org/plugins/theme-check/", + "repository": "git+https://github.com/WordPress/theme-check.git", + "bugs": { + "url": "https://wordpress.org/support/plugin/theme-check/" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + }, + "scripts": { + "update-version": "node update-version-and-changelog.js" + }, + "devDependencies": { + "simple-git": "^3.26.0" + } +} diff --git a/update-version-and-changelog.js b/update-version-and-changelog.js new file mode 100644 index 00000000..faf42223 --- /dev/null +++ b/update-version-and-changelog.js @@ -0,0 +1,159 @@ +/* eslint-disable no-console */ + +/** + * External dependencies + */ +const fs = require( 'fs' ); +const core = require( '@actions/core' ); +const simpleGit = require( 'simple-git' ); +const { promisify } = require( 'util' ); +const exec = promisify( require( 'child_process' ).exec ); + +const git = simpleGit.default(); + +const releaseType = process.env.RELEASE_TYPE; + +// Constants +const VALID_RELEASE_TYPES = [ 'major', 'minor', 'patch' ]; +const MAIN_PLUGIN_FILE = 'theme-check.php'; + +// To get the merges since the last (previous) tag +async function getChangesSinceLastTag() { + try { + // Fetch all tags, sorted by creation date + const tagsResult = await git.tags( { + '--sort': '-creatordate', + } ); + const tags = tagsResult.all; + if ( tags.length === 0 ) { + console.error( '❌ Error: No previous tags found.' ); + return null; + } + const previousTag = tags[ 0 ]; // The most recent tag + + // Now get the changes since this tag + const changes = await git.log( [ `${ previousTag }..HEAD` ] ); + return changes; + } catch ( error ) { + throw error; + } +} + +// To know if there are changes since the last tag. +// we are not using getChangesSinceGitTag because it returns the just the merges and not the commits. +// So for example if a hotfix was committed directly to trunk this function will detect it but getChangesSinceGitTag will not. +async function getHasChangesSinceGitTag( tag ) { + const changes = await git.log( [ `HEAD...${ tag }` ] ); + return changes?.all?.length > 0; +} + +async function updateVersion() { + if ( ! VALID_RELEASE_TYPES.includes( releaseType ) ) { + console.error( + '❌ Error: Release type is not valid. Valid release types are: major, minor, patch.' + ); + process.exit( 1 ); + } + + if ( + ! fs.existsSync( './package.json' ) || + ! fs.existsSync( './package-lock.json' ) + ) { + console.error( '❌ Error: package.json or lock file not found.' ); + process.exit( 1 ); + } + + if ( ! fs.existsSync( './readme.txt' ) ) { + console.error( '❌ Error: readme.txt file not found.' ); + process.exit( 1 ); + } + + if ( ! fs.existsSync( `./${ MAIN_PLUGIN_FILE }` ) ) { + console.error( `❌ Error: ${ MAIN_PLUGIN_FILE } file not found.` ); + process.exit( 1 ); + } + + // get changes since last tag + let changes = []; + try { + changes = await getChangesSinceLastTag(); + } catch ( error ) { + console.error( + `❌ Error: failed to get changes since last tag: ${ error }` + ); + process.exit( 1 ); + } + + const packageJson = require( './package.json' ); + const currentVersion = packageJson.version; + + // version bump package.json and package-lock.json using npm + const { stdout, stderr } = await exec( + `npm version --commit-hooks false --git-tag-version false ${ releaseType }` + ); + if ( stderr ) { + console.error( `❌ Error: failed to bump the version."` ); + process.exit( 1 ); + } + + const currentTag = `v${ currentVersion }`; + const newTag = stdout.trim(); + const newVersion = newTag.replace( 'v', '' ); + const hasChangesSinceGitTag = await getHasChangesSinceGitTag( currentTag ); + + // check if there are any changes + if ( ! hasChangesSinceGitTag ) { + console.error( + `❌ No changes since last tag (${ currentTag }). There is nothing new to release.` + ); + // revert version update + await exec( + `npm version --commit-hooks false --git-tag-version false ${ currentVersion }` + ); + process.exit( 1 ); + } + + console.info( 'βœ… Package.json version updated', currentTag, '=>', newTag ); + + // update readme.txt version with the new changelog + const readme = fs.readFileSync( './readme.txt', 'utf8' ); + const capitalizeFirstLetter = ( string ) => + string.charAt( 0 ).toUpperCase() + string.slice( 1 ); + + const changelogChanges = changes.all + .map( + ( change ) => + `* ${ capitalizeFirstLetter( change.message || change.body ) }` + ) + .join( '\n' ); + const newChangelog = `== Changelog ==\n\n= ${ newVersion } =\n${ changelogChanges }`; + let newReadme = readme.replace( '== Changelog ==', newChangelog ); + // update version in readme.txt + newReadme = newReadme.replace( + /Stable tag: (.*)/, + `Stable tag: ${ newVersion }` + ); + fs.writeFileSync( './readme.txt', newReadme ); + console.info( 'βœ… Readme version updated', currentTag, '=>', newTag ); + + // update theme-check.php version + const pluginPhpFile = fs.readFileSync( `./${ MAIN_PLUGIN_FILE }`, 'utf8' ); + const newPluginPhpFile = pluginPhpFile.replace( + /Version: (.*)/, + `Version: ${ newVersion }` + ); + fs.writeFileSync( `./${ MAIN_PLUGIN_FILE }`, newPluginPhpFile ); + console.info( + `βœ… ${ MAIN_PLUGIN_FILE } file version updated`, + currentTag, + '=>', + newTag + ); + + // output data to be used by the next steps of the github action + core.setOutput( 'NEW_VERSION', newVersion ); + core.setOutput( 'NEW_TAG', newTag ); + core.setOutput( 'CHANGELOG', changelogChanges ); +} + +updateVersion(); From dcb3811e17fa749128d5f2a71c912abb25623def Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 10 Oct 2024 16:07:30 -0300 Subject: [PATCH 2/2] user master instead of trunk branch name --- .github/workflows/deploy.yml | 2 +- .github/workflows/release-new-version.yml | 2 +- update-version-and-changelog.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5a688d31..e4d14ba6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,7 +4,7 @@ on: pull_request: types: [closed] branches: - - trunk + - master jobs: deploy-to-wordpress: diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index 502c0221..e6f17f80 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -61,7 +61,7 @@ jobs: run: | gh pr create \ --title "[Automation] New ${{ github.event.inputs.release_type }} Release: ${{ steps.update-version.outputs.NEW_VERSION }}" \ - --base trunk \ + --base master \ --head ${{ steps.create-branch.outputs.BRANCH_NAME }} \ --label "Release: ${{ github.event.inputs.release_type }}" \ --body " diff --git a/update-version-and-changelog.js b/update-version-and-changelog.js index faf42223..f750be6d 100644 --- a/update-version-and-changelog.js +++ b/update-version-and-changelog.js @@ -41,7 +41,7 @@ async function getChangesSinceLastTag() { // To know if there are changes since the last tag. // we are not using getChangesSinceGitTag because it returns the just the merges and not the commits. -// So for example if a hotfix was committed directly to trunk this function will detect it but getChangesSinceGitTag will not. +// So for example if a hotfix was committed directly to master this function will detect it but getChangesSinceGitTag will not. async function getHasChangesSinceGitTag( tag ) { const changes = await git.log( [ `HEAD...${ tag }` ] ); return changes?.all?.length > 0;