GitHub tag validation tool with cryptographic signature verification and GitHub key registry checks
Project description
🏷️ Unified Tag Validation Action
A comprehensive GitHub Action for validating tags across versioning schemes (Semantic Versioning and Calendar Versioning) with cryptographic signature verification (SSH and GPG).
This action unifies and extends the functionality of
tag-validate-semantic-action and tag-validate-calver-action.
Features
- ✅ Semantic Versioning (SemVer) validation
- ✅ Calendar Versioning (CalVer) validation
- ✅ SSH signature detection and verification
- ✅ GPG signature detection and verification
- ✅ Remote tag validation via GitHub API
- ✅ Local tag validation in current repository
- ✅ String validation (no signature check)
- ✅ Development/pre-release tag detection
- ✅ Version prefix (v/V) detection
- ✅ Flexible validation requirements
Quick Start
Check Current Repository Tag Push
name: "Check Tag"
on:
push:
tags:
- '*'
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Check pushed tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: semver
require_signed: true
Check Remote Repository Tag
- name: "Check remote tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "lfreleng-actions/tag-validate-action/v1.0.0"
require_type: semver
require_signed: gpg
Check Tag String
- name: "Check version string"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_string: "2025.01.15"
require_type: calver
Inputs
| Name | Required | Default | Description |
|---|---|---|---|
| tag_location | False | '' | Path to tag: remote (ORG/REPO/TAG) or local (PATH/TO/REPO/TAG) |
| tag_string | False | '' | Tag string to check (version format, signature check skipped) |
| require_type | False | none | Required tag type: semver, calver, or none |
| require_signed | False | ambivalent | Signature rule: true, ssh, gpg, false, or ambivalent |
| permit_missing | False | false | Allow missing tags without error |
| token | False | '' | GitHub token for authenticated API calls and private repository access |
| github_server_url | False | '' | GitHub server URL (for GitHub Enterprise Server) |
| debug | False | false | Enable debug output including git error messages |
Input Details
tag_location
Specifies a tag to check. Supports two formats:
- Remote repository:
ORG/REPO/TAG - Local repository:
PATH/TO/REPO/TAG
Remote Examples:
lfreleng-actions/tag-validate-action/v1.0.0lfreleng-actions/tag-validate-action/2025.01.15
Local Examples:
./my-repo/v1.0.0test-repos/semantic-tags/v2.1.0
For remote tags, the action will:
- Attempt to find the tag with the exact name provided
- If not found and the tag starts with 'v', try without the 'v' prefix
- If not found and the tag doesn't start with 'v', try with 'v' prefix added
For local paths, the repository directory must contain a .git directory.
tag_string
Validates a version string without accessing any repository. Signature checking is not performed in this mode.
Use case: Check version strings before creating tags.
require_type
Enforces the versioning scheme the tag must follow.
semver- Tag must match Semantic Versioning formatcalver- Tag must match Calendar Versioning formatnone- Any format accepted (default)
Note: Input is case-insensitive.
require_signed
Controls cryptographic signature requirements.
ambivalent- No enforcement, signature type reported as output (default)true- Tag must have a signature (SSH or GPG)ssh- Tag must be SSH-signed specificallygpg- Tag must be GPG-signed specificallyfalse- Tag must have no signature
Note: Input is case-insensitive. The action skips signature checking when
using tag_string mode.
permit_missing
When set to true, the action will not fail if:
- No tag exists in the workflow context (not a tag push event)
- The
tag_locationspecified doesn't exist - Empty
tag_stringprovided
The action will still fail if:
tag_locationformat is invalid- Required validation checks fail (type or signature mismatch)
token
GitHub token for authenticated API requests and private repository access.
Use cases:
- Access private repositories via
tag_location - Increase API rate limits (60/hour → 5,000/hour)
- Clone repositories requiring authentication
Example:
- uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "my-org/private-repo/v1.0.0"
token: ${{ secrets.GITHUB_TOKEN }}
Note: For workflows in the same repository, ${{ secrets.GITHUB_TOKEN }}
is automatically available.
github_server_url
GitHub server URL for git operations. Supports GitHub Enterprise Server.
Default behavior:
- Uses the provided
github_server_urlif specified - Falls back to
GITHUB_SERVER_URLenvironment variable - Falls back to
https://github.com
Use case: When validating tags from GitHub Enterprise Server instances.
Example:
- uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "my-org/my-repo/v1.0.0"
github_server_url: "https://github.enterprise.example.com"
debug
Enable debug output in action logs for troubleshooting.
When enabled, the action will output:
- Internal variable values
- Git command outputs and error messages
- Tag object inspection details
- Signature verification details
Use case: Diagnosing validation failures or unexpected behavior.
Example:
- uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "my-org/my-repo/v1.0.0"
debug: true
Outputs
| Name | Description |
|---|---|
| valid | Set to true if tag passes all validation checks |
| tag_type | Detected tag type: semver, calver, both, or unknown |
| signing_type | Signing method used: unsigned, ssh, gpg, gpg-unverifiable, lightweight, or invalid |
| development_tag | Set to true if tag contains pre-release/development strings |
| version_prefix | Set to true if tag has leading v/V character |
| tag_name | The tag name under inspection |
Tag Detection Priority
The action determines which tag to check in the following order:
tag_location- If provided, validates the specified remote tagtag_string- If provided (and no tag_location), validates the string- Git context - If neither above provided, checks if a tag push started the workflow
If none of the above sources provide a tag:
- With
permit_missing: true- Action succeeds with minimal outputs - With
permit_missing: false- Action fails with an error
Usage Examples
Enforce SemVer with GPG Signatures
name: "Strict Tag Validation"
on:
push:
tags:
- 'v*'
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Check tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: semver
require_signed: gpg
Check CalVer Tags (Any Signature)
- name: "Check CalVer tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: calver
require_signed: true
Check Remote Tag Before Release
- name: "Check dependency version"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "my-org/my-dependency/v2.1.0"
require_type: semver
permit_missing: false
token: ${{ secrets.GITHUB_TOKEN }}
Check Version String in CI
- name: "Check version from package.json"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_string: ${{ steps.get_version.outputs.version }}
require_type: semver
Detect Development Tags
- name: "Check tag and determine if development"
id: check
uses: lfreleng-actions/tag-validate-action@v1
- name: "Skip deployment for dev tags"
if: steps.check.outputs.development_tag == 'true'
run: echo "Skipping deployment for development tag"
Flexible Validation (No Requirements)
- name: "Detect tag properties"
id: detect
uses: lfreleng-actions/tag-validate-action@v1
with:
permit_missing: true
- name: "Show tag info"
run: |
echo "Tag Type: ${{ steps.detect.outputs.tag_type }}"
echo "Signing: ${{ steps.detect.outputs.signing_type }}"
echo "Dev Tag: ${{ steps.detect.outputs.development_tag }}"
echo "Has Prefix: ${{ steps.detect.outputs.version_prefix }}"
Check Private Repository Tag
- name: "Check private repository tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "my-org/private-repo/v2.0.0"
require_type: semver
require_signed: gpg
token: ${{ secrets.PAT_TOKEN }} # Personal Access Token with repo scope
Implementation Details
Semantic Versioning (SemVer)
Uses the official regular expression from semver.org:
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
Valid examples:
1.0.0v2.3.10.1.0-alpha.11.0.0-beta+exp.sha.5114f85
Calendar Versioning (CalVer)
Uses a flexible pattern to support different CalVer schemes:
^(\d{2}|\d{4})\.(\d{1}|\d{2})((\.|\_|-)[a-zA-Z][a-zA-Z0-9\.\-\_]*)?(\.(\d{1}|\d{2})((\.|\_|-)[a-zA-Z][a-zA-Z0-9\.\-\_]*)?)?$
Valid examples:
2025.01.1525.1.02025.1v2025.01.15-beta.1
Development Tag Detection
Detects common pre-release/development identifiers (case-insensitive):
devprealphabetarcsnapshotnightlycanarypreview
Examples:
v1.0.0-dev→development_tag: true2025.01-beta.1→development_tag: truev1.0.0→development_tag: false
Signature Detection
The action detects signatures using two methods:
GPG Signatures:
- Executes
git verify-tag --raw <tag> - Looks for
[GNUPG:]markers (GOODSIG, VALIDSIG, ERRSIG)
SSH Signatures:
- Checks for SSH-specific markers in verification output
- Examines tag object for
-----BEGIN SSH SIGNATURE-----block
Limitations:
- Signature checking requires the tag to exist in a git repository
- The action clones remote tags temporarily for signature verification
- String validation (
tag_string) cannot check signatures
Requirements
Signature Verification Result Codes
| Git Verify Result | signing_type | Description |
|---|---|---|
| 0 | gpg | GPG signature verified (GOODSIG or VALIDSIG detected) |
| non-zero | gpg-unverifiable | GPG signature present but unverifiable (ERRSIG - missing key) |
| 0 | ssh | SSH signature verified (pattern match in git verify-tag output or tag object) |
| non-zero | invalid | GPG signature present but verification failed (BADSIG - corrupted or tampered) |
| non-zero | lightweight | Lightweight tag (no tag object; not signable) |
| non-zero | unsigned | Annotated tag object present but no GPG/SSH signature markers detected |
| non-zero | unsigned | Tag object unreadable (resolution failure or repository fetch limitation) |
| non-zero | unsigned | Tag reference resolution failed (rev-parse returned empty) |
Notes:
- The action first inspects tag object presence (annotated vs lightweight).
- Git verify result alone does not classify signature state. Output markers (GOODSIG, VALIDSIG, BADSIG, ERRSIG, SSH patterns) determine
signing_type. - The "Git Verify Result" column shows internal
git verify-tagexit codes for reference -signing_typeis the actual output exposed by the action. - ERRSIG vs BADSIG distinction: ERRSIG (missing key) returns
gpg-unverifiableto allow consumers to make informed security decisions; BADSIG (failed verification) returnsinvalid. - A
lightweighttag is functionally treated as an unsigned tag for policy enforcement, but surfaced distinctly for clarity. invalidsignature states cause failure whenrequire_signedistrue,gpg, orssh.
GitHub API Response Handling
The remote tag existence check uses HTTP status codes (200 success, others treated as missing). A future enhancement will parse the JSON body to distinguish:
- Permission issues (403) vs true absence (404)
- Redirect or legacy ref patterns
- Error payloads indicating rate limiting This planned improvement will allow more precise error messaging and potentially differentiated handling (e.g. retry vs fail-fast).
Git Version
- Git 2.34 or later required for SSH signing support
- GitHub Actions runners typically have Git 2.39+
Repository Checkout
For local tag validation (tag push events):
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required to fetch all tags
GitHub Token
For remote tag validation, the action can use authenticated or anonymous API calls:
Without token:
- Rate limit: 60 requests/hour
- Cannot access private repositories
With token:
- Rate limit: 5,000 requests/hour
- Can access private repositories (with appropriate permissions)
Usage:
- uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "owner/repo/v1.0.0"
token: ${{ secrets.GITHUB_TOKEN }}
Validation Logic
Type Validation
| require_type | tag_type | Result |
|---|---|---|
none |
any | ✅ Pass |
semver |
semver |
✅ Pass |
semver |
both |
✅ Pass |
semver |
calver |
❌ Fail |
calver |
calver |
✅ Pass |
calver |
both |
✅ Pass |
calver |
semver |
❌ Fail |
Signature Validation
| require_signed | signing_type | Result |
|---|---|---|
ambivalent |
any | ✅ Pass (always) |
true |
ssh/gpg |
✅ Pass |
true |
gpg-unverifiable |
❌ Fail |
true |
unsigned |
❌ Fail |
true |
lightweight |
❌ Fail |
true |
invalid |
❌ Fail |
ssh |
ssh |
✅ Pass |
ssh |
gpg |
❌ Fail |
ssh |
gpg-unverifiable |
❌ Fail |
ssh |
unsigned |
❌ Fail |
ssh |
lightweight |
❌ Fail |
ssh |
invalid |
❌ Fail |
gpg |
gpg |
✅ Pass |
gpg |
gpg-unverifiable |
❌ Fail |
gpg |
ssh |
❌ Fail |
gpg |
unsigned |
❌ Fail |
gpg |
lightweight |
❌ Fail |
gpg |
invalid |
❌ Fail |
false |
unsigned |
✅ Pass |
false |
lightweight |
✅ Pass |
false |
ssh |
❌ Fail |
false |
gpg |
❌ Fail |
false |
gpg-unverifiable |
❌ Fail |
false |
invalid |
❌ Fail |
Security Note: Unverifiable Signatures
Important: When require_signed=true or require_signed=gpg, tags with
gpg-unverifiable signatures will fail validation. This is a security
feature to prevent tags signed with unknown or untrusted keys from bypassing
signature requirements.
Why this matters:
- A
gpg-unverifiablesignature means the key is not in your keyring - This may mean the key is untrusted or compromised
- For production releases, accept verifiable signatures
If you need to allow unverifiable signatures:
- Use
require_signed=ambivalent(accepts any signature state) - Or import the GPG key into your keyring for verification
Example workflow with key import:
- name: Import GPG keys
run: |
echo "${{ secrets.GPG_PUBLIC_KEY }}" | gpg --import
- name: Check tag signature
uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
Troubleshooting
"Tag not found" errors
Solution: When validating local tags, ensure:
- You check out the repository with
fetch-depth: 0 - The tag exists in the repository
- The tag name is correct (check for v prefix)
Signature verification fails
Possible causes:
- Not in a git repository
- Tag doesn't exist locally
- Using
tag_stringmode (signatures not checked)
Solution: Use tag push events or tag_location for signature validation.
Rate limiting on remote tags
Solution: Provide GitHub token for higher rate limits:
- uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: "owner/repo/v1.0.0"
token: ${{ secrets.GITHUB_TOKEN }}
Migration from Previous Actions
From tag-validate-semantic-action
# Old action
- uses: lfreleng-actions/tag-validate-semantic-action@v1
with:
string: ${{ github.ref_name }}
require_signed: gpg
# New unified action
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: semver
require_signed: gpg
From tag-validate-calver-action
# Old action
- uses: lfreleng-actions/tag-validate-calver-action@v1
with:
string: ${{ github.ref_name }}
exit_on_fail: true
# New unified action
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: calver
Output changes:
dev_version→development_tag- Added:
tag_type,version_prefix,tag_name
Related Projects
- tag-validate-semantic-action - SemVer validation
- tag-validate-calver-action - CalVer validation
License
Apache-2.0
Local Testing
You can test the action locally using Nektos/Act before pushing to GitHub:
# Setup (one time)
make install-act
make setup-secrets
# Run quick smoke test
make test-quick
# Run specific test suites
make test-basic
make test-local-tags
make test-signatures
make test-python
# Run all tests
make test-all
Benefits:
- ✅ Fast feedback loop (no waiting for CI)
- ✅ No GitHub Actions minutes consumed
- ✅ Easy debugging with direct container access
- ✅ Test before pushing commits
See docs/LOCAL_TESTING.md for detailed setup and usage instructions.
Contributing
Contributions are welcome! Please open an issue or pull request.
Before submitting a PR, please:
- Test locally with
make test-all - Run pre-commit hooks:
pre-commit run --all-files - Ensure all tests pass
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tag_validate-0.2.1.tar.gz.
File metadata
- Download URL: tag_validate-0.2.1.tar.gz
- Upload date:
- Size: 51.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc129d4fcc8a0e7d0b40c1c2742a0819c3714aa023175a40e2b3f9046a6e2ec5
|
|
| MD5 |
8239f3fa9b113bb223940c56b53f909d
|
|
| BLAKE2b-256 |
05336a2a2c2589a45c19cc3edf97ed6bb4b0e77a777c479f4758a0e4c453023b
|
File details
Details for the file tag_validate-0.2.1-py3-none-any.whl.
File metadata
- Download URL: tag_validate-0.2.1-py3-none-any.whl
- Upload date:
- Size: 50.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b264fa9bbdcae4539a377f3b332a7a4d8a125ab20e195da4e89b3a08f797a2d8
|
|
| MD5 |
fb9129bae14de2a3ed002fc1fe96c2fe
|
|
| BLAKE2b-256 |
f8782a9704b247fe8cd6e0be2f33d2b6ab1af83d341d60fb395ac96913ee5f19
|