January 25, 2026

Automatic WordPress Theme Versioning with Semantic Release

wordpress devops git github actions automation

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.json and package-lock.json are versioned in the repository – this way npm ci works 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 the master branch. If you switch to main in the future, just change this value.
  • prepareCmd – we use sed -i '' … (macOS) and sed -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.json is 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.md file
  • exec – runs sed, which replaces the version in style.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: npm in setup-node we 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 than npm install, but requires package-lock.json to be in the repository (which is already the case in our project).
  • style.css – make sure the theme header contains one Version: line in the format Version: x.y.z. sed will replace exactly that line.
  • Syncing version in package.json – if you want the version in package.json to always match the version in style.css, you can add an additional step in prepareCmd, e.g., npm version ${nextRelease.version} --no-git-tag-version.

How to Work with This Daily?

The best practice is the Squash & Merge strategy:

  1. Developer works on their branch (e.g., feat/new-design). They can make “dirty” commits there (wip, fix, correction).
  2. Creates a Pull Request
  3. Key moment: The PR title must follow the convention, e.g., feat: refresh homepage design
  4. When merging select the “Squash and merge” option
  5. GitHub flattens the history into one commit with the PR title
  6. Automation sees feat: ..., bumps the version in style.css, updates CHANGELOG.md and creates a Release

Conventional Commits Convention

PrefixMeaningVersion Change
feat:New featureminor (1.x.0)
fix:Bug fixpatch (1.0.x)
docs:Documentation onlynone
style:Code formattingnone
refactor:Refactoringnone
perf:Performance optimizationpatch
chore:Maintenance tasksnone
BREAKING CHANGE:Breaking changemajor (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.

Have questions or suggestions? Leave a comment below.

M
Written by

Mateusz Zadorozny

SHIFT64 Founder. WooCommerce performance specialist helping store owners achieve faster load times and better conversions.