Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate release process with a GitHub action #471

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 49 additions & 25 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
name: Deploy to WordPress.org

on:
release:
types: [published]
pull_request:
types: [closed]
branches:
- master

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 }}
71 changes: 71 additions & 0 deletions .github/workflows/release-new-version.yml
Original file line number Diff line number Diff line change
@@ -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 master \
--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."
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
159 changes: 159 additions & 0 deletions update-version-and-changelog.js
Original file line number Diff line number Diff line change
@@ -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 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;
}

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();
Loading