Every time you publish a new version of a GitHub Action, say v1.2.3, it's customary to also update the tags for v1.2 and v1 to point to the same commit. That way people can subscribe to either an exact version or a floating version that's automatically updated when the action's author pushes a new version.
Unfortunately, GitHub's creative use of tags doesn't do this automatically and many actions don't auto-update their major and minor versions whenever they release a new patch.
You can run this action for your GitHub Action's repository to ensure the correct tags have been created and point to the correct commits.
- uses: jessehouwing/actions-semver-checker@v2
with:
# GitHub token for API access. Falls back to GITHUB_TOKEN if not provided.
# Required for auto-fix mode
token: ${{ secrets.GITHUB_TOKEN }}
# Check minor version tags (v1.0) in addition to major tags (v1)
# Options: error, warning, none, true, false
# Default: error
check-minor-version: 'error'
# Require GitHub Releases for every patch version (v1.0.0)
# Options: error, warning, none, true, false
# Default: error
check-releases: 'error'
# Ensure releases are published (immutable), not drafts
# Options: error, warning, none, true, false
# Default: error
check-release-immutability: 'error'
# Validate GitHub Marketplace metadata and publication
# Checks action.yaml has name, description, branding; README.md exists
# Options: error, warning, none, true, false
# Default: error
check-marketplace: 'error'
# Exclude preview/pre-release versions from floating version calculations
# Options: true, false
# Default: true
ignore-preview-releases: 'true'
# Use tags or branches for floating versions (v1, v1.0, latest)
# Options: tags, branches
# Default: tags
floating-versions-use: 'tags'
# Automatically fix detected issues. Requires contents: write permission
# Options: true, false
# Default: false
auto-fix: 'false'
# Comma-separated list of versions to skip during validation
# Supports wildcards (e.g., 'v1.*')
# Default: "" (empty)
ignore-versions: ''This action implements GitHub's recommended approach for versioning actions:
- Patch versions (v1.0.0, v1.0.1) → Immutable GitHub Releases that never change
- Floating versions (v1, v1.0, latest) → Mutable Git tags that point to the latest compatible release
This strategy balances stability (pinned versions never change) with convenience (floating versions get updates automatically).
📖 Read the comprehensive versioning guidance for detailed best practices, workflows, and troubleshooting.
- ✅ Validates that major (
v1) and minor (v1.0) version tags point to the latest patch version - ✅ Checks that every patch version (
v1.0.0) has a corresponding GitHub Release (via REST API) - ✅ Verifies that releases are immutable (not in draft status)
- ✅ Supports filtering preview/pre-release versions from major/minor tag calculations
- ✅ Can configure floating versions (major/minor/latest) to use branches or tags
- ✅ Provides suggested commands to fix any issues with direct links to GitHub release pages
- ✅ Optional auto-fix mode to automatically update version tags/branches
- ✅ NEW in v2: No checkout required - uses GitHub REST API exclusively
- ✅ Ignore specific versions from validation (useful for legacy versions)
- ✅ Auto-fix automatically republishes non-immutable releases to make them immutable (when
check-release-immutabilityis enabled) - ✅ Retry logic with exponential backoff for better reliability
- ✅ Validates GitHub Marketplace metadata (action name, description, branding, README)
- ✅ Checks that releases are published to the GitHub Marketplace
Example output:
🔴 Manual Remediation Required
Version: v1 ref 59499a44cd4482b68a7e989a5e7dd781414facfa must match: v1.0.6 ref 1a13fd188ebef96fb179faedfabcc8de5cb6189d Version: v1.0 ref 59499a44cd4482b68a7e989a5e7dd781414facfa must match: v1.0.6 ref 1a13fd188ebef96fb179faedfabcc8de5cb6189d Version: latest ref 59499a44cd4482b68a7e989a5e7dd781414facfa must match: v1.0.6 ref 1a13fd188ebef96fb179faedfabcc8de5cb6189d
And a set of suggested Git commands to fix this:
git push origin 1a13fd188ebef96fb179faedfabcc8de5cb6189d:refs/tags/v1 --force git push origin 1a13fd188ebef96fb179faedfabcc8de5cb6189d:refs/tags/v1.0 --force git push origin 1a13fd188ebef96fb179faedfabcc8de5cb6189d:latest --force
In addition to running as a GitHub Action, you can use this tool from the command line via the GitHubActionVersioning PowerShell module.
Install from the PowerShell Gallery:
Install-Module -Name GitHubActionVersioning -Scope CurrentUser# Import the module
Import-Module GitHubActionVersioning
# Validate a repository (uses GitHub CLI token automatically)
Test-GitHubActionVersioning -Owner 'owner' -Repo 'repo'
# Validate with auto-fix enabled
Test-GitHubActionVersioning -Owner 'owner' -Repo 'repo' -AutoFix
# Use a specific token
Test-GitHubActionVersioning -Owner 'owner' -Repo 'repo' -Token $env:GITHUB_TOKEN📖 Full CLI documentation for detailed usage, parameters, and examples.
The action uses a modular rule-based validation system. Each rule can be configured independently via action inputs.
| Rule | Description | Documentation |
|---|---|---|
tag_should_be_branch |
Validates that floating versions use branches when configured for branches mode | 📖 Details |
branch_should_be_tag |
Validates that floating versions use tags when configured for tags mode | 📖 Details |
duplicate_floating_version_ref |
Detects when a floating version exists as both tag and branch | 📖 Details |
duplicate_latest_ref |
Detects when 'latest' exists as both tag and branch | 📖 Details |
duplicate_patch_version_ref |
Detects when a patch version exists as both tag and branch | 📖 Details |
| Rule | Description | Documentation |
|---|---|---|
patch_release_required |
Ensures every patch version has a GitHub Release | 📖 Details |
release_should_be_published |
Validates that releases are published, not drafts | 📖 Details |
release_should_be_immutable |
Ensures releases are truly immutable | 📖 Details |
highest_patch_release_should_be_latest |
Ensures the correct release is marked as 'latest' | 📖 Details |
floating_version_no_release |
Warns when a release exists for a floating version | 📖 Details |
duplicate_release |
Detects and removes duplicate draft releases for the same tag | 📖 Details |
| Rule | Description | Documentation |
|---|---|---|
major_tag_missing |
Detects missing major version tags (v1, v2) | 📖 Details |
major_tag_tracks_highest_patch |
Ensures major tags point to latest patch | 📖 Details |
major_branch_missing |
Detects missing major version branches | 📖 Details |
major_branch_tracks_highest_patch |
Ensures major branches point to latest patch | 📖 Details |
minor_tag_missing |
Detects missing minor version tags (v1.0, v1.1) | 📖 Details |
minor_tag_tracks_highest_patch |
Ensures minor tags point to latest patch | 📖 Details |
minor_branch_missing |
Detects missing minor version branches | 📖 Details |
minor_branch_tracks_highest_patch |
Ensures minor branches point to latest patch | 📖 Details |
patch_tag_missing |
Detects missing patch version tags (v1.0.0) | 📖 Details |
| Rule | Description | Documentation |
|---|---|---|
latest_tag_tracks_global_highest |
Ensures 'latest' tag points to the globally highest patch | 📖 Details |
latest_branch_tracks_global_highest |
Ensures 'latest' branch points to the globally highest patch | 📖 Details |
| Rule | Description | Documentation |
|---|---|---|
action_metadata_required |
Validates action.yaml has required marketplace metadata (name, description, branding) and README.md exists | 📖 Details |
marketplace_publication_required |
Verifies the latest release has been published to the GitHub Marketplace | 📖 Details |
When auto-fix: true is enabled, the action can automatically remediate issues using these actions:
| Action | Description | Documentation |
|---|---|---|
CreateTagAction |
Creates a new Git tag at a specified commit | 📖 Details |
UpdateTagAction |
Updates an existing tag to point to a different commit | 📖 Details |
DeleteTagAction |
Deletes an existing tag | 📖 Details |
| Action | Description | Documentation |
|---|---|---|
CreateBranchAction |
Creates a new branch at a specified commit | 📖 Details |
UpdateBranchAction |
Updates an existing branch to point to a different commit | 📖 Details |
DeleteBranchAction |
Deletes an existing branch | 📖 Details |
| Action | Description | Documentation |
|---|---|---|
CreateReleaseAction |
Creates a new GitHub Release for a tag | 📖 Details |
PublishReleaseAction |
Publishes a draft release | 📖 Details |
RepublishReleaseAction |
Republishes a release to make it immutable | 📖 Details |
SetLatestReleaseAction |
Sets a release as the 'latest' release | 📖 Details |
DeleteReleaseAction |
Deletes an existing release | 📖 Details |
| Action | Description | Documentation |
|---|---|---|
ConvertTagToBranchAction |
Converts a tag to a branch | 📖 Details |
ConvertBranchToTagAction |
Converts a branch to a tag | 📖 Details |
No checkout required! Version 2 uses the GitHub REST API exclusively, so you don't need to checkout the repository:
- uses: jessehouwing/actions-semver-checker@v2This is a significant improvement over v1, making the action faster and simpler to use.
Version 1 requires full git history and tags:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Required for v1: Fetches full git history
fetch-tags: true # Required for v1: Fetches all tagsIf you're using the auto-fix feature to automatically update version tags/branches:
jobs:
check-semver:
permissions:
contents: write # Required for auto-fix to push tags/branches
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
auto-fix: true
token: ${{ secrets.GITHUB_TOKEN }} # Required for auto-fix API callsRequirements:
contents: writepermission - Required to push tag/branch updates via REST APItoken- GitHub token for API calls (create releases, update refs)
The default GITHUB_TOKEN cannot push tags or branches that would modify files in .github/workflows/. This is a security feature of GitHub Actions. If your action repository has workflow files that change between versions, auto-fix will fail with a permission error.
To work around this limitation, you can use either:
- GitHub App Token (Recommended for organizations)
- Fine-grained Personal Access Token (Simpler for personal repositories)
Using a GitHub App provides the most secure and manageable approach, especially for organizations.
Step 1: Create a GitHub App with the following permissions:
- Repository permissions:
Contents: Read and writeWorkflows: Read and write (this is the key permission)Metadata: Read-only (automatically selected)
Step 2: Install the App on your repository
Step 3: Use the actions/create-github-app-token action:
name: Auto-fix SemVer
on:
push:
tags:
- 'v*.*.*'
jobs:
fix-semver:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Scope the token to only this repository
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
- uses: jessehouwing/actions-semver-checker@v2
with:
auto-fix: true
token: ${{ steps.app-token.outputs.token }}Benefits:
- Fine-grained permissions scoped to specific repositories
- Can be managed at organization level
- Token automatically expires (more secure than PATs)
- Audit logging for all actions taken
For personal repositories or simpler setups, a fine-grained PAT works well.
Step 1: Create a Fine-grained PAT at GitHub Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
Step 2: Configure the token with these permissions:
- Repository access: Select your action repository
- Repository permissions:
Contents: Read and writeWorkflows: Read and write (this is the key permission)Metadata: Read-only (automatically selected)
Step 3: Add the token as a repository secret (e.g., SEMVER_TOKEN)
Step 4: Use the token in your workflow:
name: Auto-fix SemVer
on:
push:
tags:
- 'v*.*.*'
jobs:
fix-semver:
runs-on: ubuntu-latest
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
auto-fix: true
token: ${{ secrets.SEMVER_TOKEN }}
⚠️ Security Note: PATs are tied to a user account. If the user leaves the organization or their account is compromised, the token may need to be rotated. For organizations, GitHub App tokens are preferred.
You need a custom token with workflows permission if:
- Your action repository contains
.github/workflows/files - These workflow files change between versions
- You want to use
auto-fix: trueto push tags/branches
If your repository has no workflow files, or workflow files don't change between versions, the default GITHUB_TOKEN with contents: write permission is sufficient.
If auto-fix fails with a permission error like:
refusing to allow a GitHub App to create or update workflow
This indicates the token lacks workflows permission. Follow one of the options above to use a properly configured token.
- uses: jessehouwing/actions-semver-checker@v2Default: "" (uses GITHUB_TOKEN)
GitHub token for API access. If not provided, falls back to the GITHUB_TOKEN environment variable.
- uses: jessehouwing/actions-semver-checker@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}Default: error
Configures whether to check minor versions (e.g., v1.0) in addition to major versions.
Options: error, warning, or none
errorortrue: Report as error and fail the actionwarning: Report as warning but don't failnoneorfalse: Skip this check entirely
- uses: jessehouwing/actions-semver-checker@v2
with:
check-minor-version: 'error'Default: error
Check that every build version (e.g., v1.0.0) has a corresponding GitHub Release.
Options: error, warning, or none
errorortrue: Report as error and fail the actionwarning: Report as warning but don't failnoneorfalse: Skip this check entirely
⚠️ Permission Note: Draft releases are NOT visible withcontents: readpermission. If you have draft releases and this check is enabled, you must usecontents: writepermission or the action will incorrectly report them as missing.
- uses: jessehouwing/actions-semver-checker@v2
with:
check-releases: 'error'Default: error
Check that releases are immutable (not in draft status). Draft releases allow tag changes, making them mutable.
Options: error, warning, or none
errorortrue: Report as error and fail the actionwarning: Report as warning but don't failnoneorfalse: Skip this check entirely
- uses: jessehouwing/actions-semver-checker@v2
with:
check-release-immutability: 'error'Default: true ✅ Recommended
Ignore preview/pre-release versions when calculating which version major/minor tags should point to. When enabled (default):
- Preview releases are excluded from
v1andv1.0tag calculations - If
v1.1.1(stable) exists andv1.1.2(preview),v1andv1.1will point tov1.1.1
# Default behavior (preview releases ignored)
- uses: jessehouwing/actions-semver-checker@v2
# Or explicitly set to false to include preview releases
- uses: jessehouwing/actions-semver-checker@v2
with:
ignore-preview-releases: 'false'Default: tags
Specify whether floating versions (major like v1, minor like v1.0, and latest) should use tags or branches. This is useful when you want mutable major/minor versions that can be updated via branch commits.
Options: tags or branches
- uses: jessehouwing/actions-semver-checker@v2
with:
floating-versions-use: 'branches'Default: false
Automatically fix major/minor version tags or branches when a build tag is pushed.
contents: write permission:
jobs:
check-semver:
permissions:
contents: write # Required for auto-fix
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
auto-fix: 'true'Note:
- Auto-fix handles all operations via REST API (no checkout required)
- When
check-release-immutabilityis set toerrororwarning(default), auto-fix will also automatically republish non-immutable releases to make them immutable
Auto-fix behavior for releases:
When auto-fix: true is enabled and check-release-immutability is set to error or warning:
- Creates releases for missing patch versions (vX.Y.Z)
- Attempts to publish draft releases automatically
- Republishes non-immutable releases by temporarily converting them to draft and publishing again to make them immutable
This automatic republishing helps migrate repositories to GitHub's immutable release strategy without manual intervention.
Default: "" (empty)
List of versions to ignore during validation. This is useful for skipping legacy or problematic versions that you don't want to validate.
Supported formats:
# Comma-separated
ignore-versions: 'v1.0.0,v2.0.0,v3.0.0'
# Newline-separated (using YAML literal block)
ignore-versions: |
v1.0.0
v2.0.0
v3.0.0
# JSON array
ignore-versions: '["v1.0.0", "v2.0.0", "v3.0.0"]'Wildcard support:
You can use wildcards to match multiple versions:
# Ignore all v1.x versions
ignore-versions: 'v1.*'
# Ignore all preview releases and specific version
ignore-versions: 'v1.0.0'Use cases:
- Skip validation for legacy versions that don't follow current standards
- Ignore problematic versions that can't be fixed
- Exclude pre-release versions from validation
- Bulk ignore version ranges using wildcards
name: Check SemVer
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
check-semver:
permissions:
contents: read
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
check-minor-version: 'true'
check-releases: 'true'
check-release-immutability: 'true'name: Auto-fix SemVer
on:
push:
tags:
- 'v*.*.*' # Only trigger on patch versions
jobs:
fix-semver:
permissions:
contents: write # Required for auto-fix
runs-on: ubuntu-latest
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
auto-fix: 'true'name: Check SemVer (Stable Only)
on:
push:
tags:
- 'v*'
jobs:
check-semver:
runs-on: ubuntu-latest
steps:
- uses: jessehouwing/actions-semver-checker@v2
with:
check-releases: 'error'
ignore-preview-releases: falseWhen issues are detected, the action provides specific commands to fix them, including direct links to GitHub release pages:
gh release create v1.0.0 --draft --title "v1.0.0" --notes "Release v1.0.0"
gh release edit v1.0.0 --draft=false # Or edit at: https://github.com/{owner}/{repo}/releases/edit/v1.0.0git push origin <sha>:refs/tags/v1 --force
git push origin <sha>:refs/tags/v1.0 --forcegit push origin <sha>:refs/heads/v1 --force
git push origin <sha>:refs/heads/v1.0 --force
git push origin <sha>:refs/heads/latest --forceRequires contents: read permission to retrieve tags, branches and published releases.
Note: Draft releases are NOT visible with
contents: readpermission. If you have draft releases andcheck-releasesis enabled, the action will incorrectly report them as missing. Usecontents: writeto see draft releases.
Requires contents: write permission to push tag/branch updates and see draft releases:
jobs:
check-semver:
permissions:
contents: writeThis action uses a modular architecture with a rule-based validation system, making it maintainable, testable, and extensible.
actions-semver-checker/
├── main.ps1 # Orchestrator (~350 lines)
│ ├── Initialize State
│ ├── Collect tags, branches, releases via GitHub API
│ ├── Load and execute validation rules
│ ├── Execute remediation actions (auto-fix)
│ └── Report results
│
├── lib/ # Core modules (~3,246 lines total)
│ ├── StateModel.ps1 # Domain model (~639 lines)
│ │ ├── VersionRef class - Represents version tags/branches
│ │ ├── ReleaseInfo class - GitHub release metadata
│ │ ├── ValidationIssue class - Tracks issues with status
│ │ ├── RepositoryState class - Single source of truth
│ │ └── RemediationPlan class - Dependency ordering
│ │
│ ├── GitHubApi.ps1 # GitHub REST API client (~1,165 lines)
│ │ ├── Get releases with pagination
│ │ ├── Create/update/delete tags and branches
│ │ ├── Manage releases (create, publish, delete)
│ │ ├── Retry logic with exponential backoff
│ │ └── Handle rate limiting and errors
│ │
│ ├── ValidationRules.ps1 # Rule engine (~163 lines)
│ │ ├── ValidationRule base class
│ │ ├── Get-ValidationRule - Auto-discovery
│ │ ├── Invoke-ValidationRule - Execute rules
│ │ └── Helper functions for rule execution
│ │
│ ├── RemediationActions.ps1 # Action base (~48 lines)
│ │ └── RemediationAction base class
│ │
│ ├── Remediation.ps1 # Auto-fix coordination (~301 lines)
│ │ ├── Execute fixes via REST API
│ │ ├── Handle HTTP 422 (unfixable) errors
│ │ ├── Calculate next available versions
│ │ └── Generate manual fix commands
│ │
│ ├── InputValidation.ps1 # Input parsing (~325 lines)
│ │ ├── Read-ActionInput - Parse from environment
│ │ ├── Test-ActionInput - Validate configuration
│ │ └── Write-InputDebugInfo - Debug output
│ │
│ ├── Logging.ps1 # Safe output (~105 lines)
│ │ ├── Workflow command injection protection
│ │ └── GitHub Actions formatting helpers
│ │
│ └── VersionParser.ps1 # Version parsing (~150 lines)
│ └── ConvertTo-Version - Semantic version parsing
│
├── lib/rules/ # Validation rules (22 rules organized by category)
│ ├── ref_type/ # Reference type validation (5 rules)
│ │ ├── tag_should_be_branch/
│ │ ├── branch_should_be_tag/
│ │ ├── duplicate_floating_version_ref/
│ │ ├── duplicate_latest_ref/
│ │ └── duplicate_patch_version_ref/
│ │
│ ├── releases/ # Release validation (6 rules)
│ │ ├── patch_release_required/
│ │ ├── release_should_be_published/
│ │ ├── release_should_be_immutable/
│ │ ├── highest_patch_release_should_be_latest/
│ │ ├── floating_version_no_release/
│ │ └── duplicate_release/
│ │
│ ├── version_tracking/ # Version tracking (9 rules)
│ │ ├── major_tag_missing/
│ │ ├── major_tag_tracks_highest_patch/
│ │ ├── major_branch_missing/
│ │ ├── major_branch_tracks_highest_patch/
│ │ ├── minor_tag_missing/
│ │ ├── minor_tag_tracks_highest_patch/
│ │ ├── minor_branch_missing/
│ │ ├── minor_branch_tracks_highest_patch/
│ │ └── patch_tag_missing/
│ │
│ └── latest/ # Latest version tracking (2 rules)
│ ├── latest_tag_tracks_global_highest/
│ └── latest_branch_tracks_global_highest/
│
└── lib/actions/ # Remediation actions (14 actions organized by type)
├── base/ # Base class and documentation
├── tags/ # Tag operations (3 actions)
│ ├── CreateTagAction/
│ ├── UpdateTagAction/
│ └── DeleteTagAction/
│
├── branches/ # Branch operations (3 actions)
│ ├── CreateBranchAction/
│ ├── UpdateBranchAction/
│ └── DeleteBranchAction/
│
├── releases/ # Release operations (5 actions)
│ ├── CreateReleaseAction/
│ ├── PublishReleaseAction/
│ ├── RepublishReleaseAction/
│ ├── SetLatestReleaseAction/
│ └── DeleteReleaseAction/
│
└── conversions/ # Type conversions (2 actions)
├── ConvertTagToBranchAction/
└── ConvertBranchToTagAction/
Want to contribute? See CONTRIBUTING.md for:
- Development setup
- Module guide with detailed responsibilities
- Testing guidelines
- Code style conventions
- Pull request process
v2 is backward compatible with v1. The main differences:
-
New input options:
token- Explicit GitHub token inputcheck-releases- Now accepts "error" (default), "warning", or "none"check-release-immutability- Now accepts "error" (default), "warning", or "none"ignore-versions- Comma-separated list of versions to ignore (NEW in v2.1)
-
Configuration improvements:
floating-versions-usereplacesuse-branches- Now acceptstags(default) orbranches- Release suggestions include direct GitHub edit links
- Link header-based pagination for better API performance
- Retry logic with exponential backoff for better reliability (NEW in v2.1)
-
Opt-in features:
ignore-preview-releases: true(default) - Set tofalseto include prereleases in floating version calculationsfloating-versions-use: tags(default) - Set tobranchesto use branches for floating versionsauto-fix: false(default) - Set totrueto automatically fix missing/incorrect tags- NEW in v2.1: When
auto-fix: trueandcheck-release-immutabilityis enabled, automatically republishes non-immutable releases to make them immutable
If you want warnings instead of errors for release checks:
- uses: jessehouwing/actions-semver-checker@v2
with:
check-releases: 'warning'
check-release-immutability: 'warning'To disable release checks entirely (v1 behavior):
- uses: jessehouwing/actions-semver-checker@v2
with:
check-releases: 'none'
check-release-immutability: 'none'To skip validation for specific versions:
- uses: jessehouwing/actions-semver-checker@v2
with:
ignore-versions: 'v1.0.0,v2.0.0'To automatically fix issues including republishing non-immutable releases:
- uses: jessehouwing/actions-semver-checker@v2
with:
auto-fix: 'true'
check-release-immutability: 'error' # or 'warning' - enables automatic republishing