Anyone who works with WordPress themes knows this pain: manually editing versions in style.css, writing changelogs, remembering all the files when merging. What if I told you that you can automate all of this in 15 minutes?
The Problem with WordPress Versioning
WordPress stores the theme version in a comment in the style.css file, not in package.json like most modern projects. This means standard automatic versioning tools don’t work out-of-the-box.
The solution is Semantic Release with the right configuration.
Step 1: Install Dependencies
First, make sure a package.json file exists in your project directory. If not, initialize an npm project:
npm init -y
Then install all the required packages:
npm i -D semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/exec @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/github
Note:
package.jsonandpackage-lock.jsonare versioned in the repository – this waynpm ciworks fast and deterministically in CI.
Step 2: Configure .releaserc.json
This is the heart of our automation. Create a .releaserc.json file in the project root. The biggest challenge with WordPress is that the version isn’t in JSON, but in a comment in style.css. We’ll use the @semantic-release/exec plugin and the sed command for this.
{
"branches": ["master"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
[
"@semantic-release/exec",
{
"prepareCmd": "sed -i '' 's/^Version: .*/Version: ${nextRelease.version}/' style.css || sed -i 's/^Version: .*/Version: ${nextRelease.version}/' style.css"
}
],
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"style.css",
"package.json",
"package-lock.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}
Why This Way?
branches: ["master"]– in our project we work on themasterbranch. If you switch tomainin the future, just change this value.prepareCmd– we usesed -i '' …(macOS) andsed -i …(Linux) syntax in one command so it works on both platforms. This way you don’t have to worry about system differences.assets– we list all files that need to be committed in the release commit.package.jsonis versioned, so it stays in the list.
What Does Each Plugin Do?
- commit-analyzer – analyzes commits according to Conventional Commits and decides on version type (patch/minor/major)
- release-notes-generator – automatically generates release notes
- changelog – updates the
CHANGELOG.mdfile - exec – runs
sed, which replaces the version instyle.css - git – commits changes and creates a tag
- github – publishes the release on GitHub
Step 3: GitHub Actions Workflow
Create a file .github/workflows/release.yml. This workflow will only run when code lands on the master branch.
name: Release
on:
push:
branches:
- master
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for proper analysis
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: npm # speeds up dependency installation
- name: Install dependencies
run: npm ci # requires package-lock.json to be in the repository
# Optional step – lint and security audit before release
- name: Lint & Security Audit
run: |
npm run lint || echo "No lint script"
npm audit --audit-level=high || echo "Audit completed successfully"
- name: Run Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
Additional Notes
- npm cache – thanks to
cache: npminsetup-nodewe speed up subsequent builds. - lint/audit step – not mandatory, but worth having to check code quality and dependency security before releasing a new version.
npm ci– works faster thannpm install, but requirespackage-lock.jsonto be in the repository (which is already the case in our project).style.css– make sure the theme header contains oneVersion:line in the formatVersion: x.y.z.sedwill replace exactly that line.- Syncing version in
package.json– if you want the version inpackage.jsonto always match the version instyle.css, you can add an additional step inprepareCmd, e.g.,npm version ${nextRelease.version} --no-git-tag-version.
How to Work with This Daily?
The best practice is the Squash & Merge strategy:
- Developer works on their branch (e.g.,
feat/new-design). They can make “dirty” commits there (wip,fix,correction). - Creates a Pull Request
- Key moment: The PR title must follow the convention, e.g.,
feat: refresh homepage design - When merging select the “Squash and merge” option
- GitHub flattens the history into one commit with the PR title
- Automation sees
feat: ..., bumps the version instyle.css, updatesCHANGELOG.mdand creates a Release
Conventional Commits Convention
| Prefix | Meaning | Version Change |
|---|---|---|
feat: | New feature | minor (1.x.0) |
fix: | Bug fix | patch (1.0.x) |
docs: | Documentation only | none |
style: | Code formatting | none |
refactor: | Refactoring | none |
perf: | Performance optimization | patch |
chore: | Maintenance tasks | none |
BREAKING CHANGE: | Breaking change | major (x.0.0) |
Pro-tip: How to Enforce Proper PR Names?
People forget conventions. It’s worth adding a bot that “yells” when the PR name is incorrect. Create a file .github/workflows/pr-lint.yml:
name: "Lint PR"
on:
pull_request:
types:
- opened
- edited
- synchronize
branches:
- master
permissions:
pull-requests: read
statuses: write
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
requireScope: false
Now the Merge button will be blocked until the author fixes the title to something like fix: typo in footer.
Example Flow
sequenceDiagram
participant Dev as Developer
participant Branch as Feature Branch
participant PR as Pull Request
participant Master as Master
participant GHA as GitHub Actions
participant Release as Release
Dev->>Branch: git checkout -b feat/new-header
Dev->>Branch: Work commits (wip, fix, etc.)
Dev->>PR: Creates PR with title "feat: new header"
PR->>PR: Lint checks title ✓
PR->>Master: Squash & Merge
Master->>GHA: Trigger workflow
GHA->>GHA: Analyze commits
GHA->>GHA: Bump version in style.css
GHA->>GHA: Update CHANGELOG.md
GHA->>Release: Publish v1.2.0
Summary
Implementing this process takes about 15 minutes, but saves hours of frustration with each deployment:
- No more conflicts in
style.css - No more manual changelog writing
- Automatic tags and releases
- Enforced naming convention
Your team can focus on coding while the robots handle the bureaucracy.
Useful Links
- Semantic Release - documentation
- Conventional Commits - specification
- action-semantic-pull-request
- GitHub Actions - documentation
Have questions or suggestions? Leave a comment below.