Semantic versioning and signed Git tags are the professional standard for releasing WordPress plugins. A signed tag proves the release came from a trusted key, while a structured version number tells users and automated update systems exactly what kind of change to expect.
Problem: A WordPress plugin has a release process that relies on manually copying files and writing release notes — version numbers drift between readme.txt, the main PHP file header, and the changelog, and it is easy to forget a step.
Solution: Use annotated Git tags as the single source of truth for releases — create a tag with git tag -a v1.2.3 -m "Release notes", push it with git push origin v1.2.3, and trigger a GitHub Actions workflow on on: push: tags: ['v*'] to build, zip, and publish the plugin automatically.
The examples below create annotated and GPG-signed tags, automate version bumping in plugin headers, push releases to GitHub, and configure a GitHub Action that creates a release whenever a tag is pushed.
# Semantic versioning: MAJOR.MINOR.PATCH
# MAJOR — breaking change (incompatible API)
# MINOR — new feature (backward-compatible)
# PATCH — bug fix (backward-compatible)
# Create a lightweight tag (no message — avoid for releases)
git tag v1.2.3
# Create an annotated tag (recommended — stores tagger, date, message)
git tag -a v1.2.3 -m "Release v1.2.3: fix cart nonce expiry on checkout"
# Create a GPG-signed tag (requires a GPG key configured in git)
git config user.signingkey YOUR_GPG_KEY_ID
git tag -s v1.2.3 -m "Release v1.2.3: fix cart nonce expiry"
# Verify a signed tag
git tag -v v1.2.3
# List all tags sorted by version
git tag --sort=-version:refname | head -10
# Push a single tag to origin
git push origin v1.2.3
# Push all local tags at once
git push origin --tags
Automate version bumping in plugin headers and create a GitHub release via Actions:
#!/usr/bin/env bash
# release.sh — bump version, tag, and push
set -euo pipefail
NEW_VERSION="${1:?Usage: $0 }" # e.g., 1.2.3
PLUGIN_FILE="my-plugin.php"
README="readme.txt"
# Update version in plugin header
sed -i "s/^ \* Version:.*/ * Version: ${NEW_VERSION}/" "${PLUGIN_FILE}"
sed -i "s/^define( 'MY_PLUGIN_VERSION'.*/define( 'MY_PLUGIN_VERSION', '${NEW_VERSION}' );/" "${PLUGIN_FILE}"
# Update readme.txt Stable tag
sed -i "s/^Stable tag:.*/Stable tag: ${NEW_VERSION}/" "${README}"
git add "${PLUGIN_FILE}" "${README}"
git commit -m "chore: bump version to ${NEW_VERSION}"
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push origin HEAD --tags
echo "Released v${NEW_VERSION}"
# .github/workflows/release.yml
# Creates a GitHub Release with a zip file when a v* tag is pushed
name: Create Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build plugin zip
run: |
mkdir -p dist
rsync -r --exclude='.git' --exclude='node_modules' --exclude='tests' --exclude='.github' ./ dist/my-plugin/
cd dist && zip -r ../my-plugin-${{ github.ref_name }}.zip my-plugin/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: my-plugin-${{ github.ref_name }}.zip
generate_release_notes: true # auto-generates notes from commits
NOTE: Tag before merging to main — create the tag on the release commit, not on a work-in-progress commit. Use git describe --tags --abbrev=0 in your build scripts to read the current version without parsing the plugin header file.