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.
- ✅ 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
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: gpg- name: "Check local tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: v1.0.0
require_type: semver
require_signed: gpg- name: "Check tag with Gerrit verification"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: v1.0.0
require_type: semver
require_signed: gpg
require_gerrit: 'true' # Auto-discovers gerrit.[org].org- name: "Check tag with dual verification"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_location: v1.0.0
require_github: 'true'
require_gerrit: 'gerrit.onap.org'
require_owner: 'maintainer@project.org'
token: ${{ secrets.GITHUB_TOKEN }}- name: "Check version string"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_string: "2025.01.15"
require_type: calver| 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 | '' | Required tag type: semver, calver, both, none (comma-separated) |
| require_signed | False | '' | Signature type: gpg, ssh, gpg-unverifiable, unsigned |
| require_github | False | false | Requires that signing key is registered to a GitHub account |
| require_gerrit | False | false | Requires that signing key is registered to a Gerrit account (true for auto-discovery, or server hostname) |
| require_owner | False | '' | GitHub/Gerrit username(s)/email(s) that must own signing key |
| reject_development | False | false | Reject development/pre-release tags (alpha, beta, rc, dev, etc.) |
| permit_missing | False | false | Allow missing tags without error |
| token | False | '' | GitHub token for authenticated API calls and private repo access |
| github_server_url | False | '' | GitHub server URL (for GitHub Enterprise Server) |
| debug | False | false | Enable debug output including git error messages |
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.
Validates a version string without accessing any repository. Signature checking is not performed in this mode.
Use case: Check version strings before creating tags.
Enforces the versioning scheme the tag must follow. Accepts comma-separated values.
Version type is always detected and reported in outputs, regardless of this setting. This has negligible performance impact (regex matching, no external calls).
🎯 Version Types
| Type | Meaning | Example |
|---|---|---|
semver |
Semantic Versioning | v1.2.3 |
calver |
Calendar Versioning | 2024.01.15 |
both |
Valid as both | 2024.1.0 |
other |
Custom format | release-2024-q1 |
Examples:
require_type: semver- Requires SemVerrequire_type: semver,calver- Accepts either SemVer or CalVerrequire_type: both- Requires tags valid as both SemVer and CalVerrequire_type: noneor omit - Accepts any format (semver, calver, or custom)
Important: When omitted, custom tag formats (type: other) are accepted.
This enables signature validation for repositories using custom tagging
schemes.
Note: Input is case-insensitive.
Controls cryptographic signature types. Accepts comma-separated values.
🔐 Signature Types
| Type | Meaning | Example Use Case |
|---|---|---|
gpg |
GPG-signed with verifiable signature | Production releases |
ssh |
SSH-signed with verifiable signature | Development workflows |
gpg-unverifiable |
GPG-signed (verification not required) | Legacy signatures |
unsigned |
Must have no signature | Lightweight tags |
lightweight |
Lightweight tag (output) | Auto-generated tags |
invalid |
Invalid signature (output) | Failed verification |
Examples:
require_signed: gpg- Requires verified GPG signaturerequire_signed: gpg,ssh- Accepts either verified GPG or SSH signaturerequire_signed: unsigned- Requires no signature
Note: Input is case-insensitive. The action skips signature checking when
using tag_string mode.
When set to true, requires that the signing key is registered to a GitHub account.
This verifies that the key used to sign the tag is associated with any GitHub user.
Requires: A GitHub token must be provided via the token input.
Example:
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
require_github: true
token: ${{ secrets.GITHUB_TOKEN }}GitHub Username Auto-Detection:
When require_github is enabled but no specific username is provided via
require_owner, the action will attempt to automatically detect the GitHub
username from the tagger's email address found in the tag signature. If
successful, the username will be displayed with an [enumerated] indicator in
the output to show auto-detection rather than explicit specification.
Note: Use in combination with require_owner to verify the key belongs to
specific GitHub user(s).
Specifies one or more GitHub usernames or email addresses that must own the signing key. Accepts comma or space-separated values.
When specified, the action verifies that the key used to sign the tag is registered to one of the provided GitHub accounts or email addresses.
Requires: A GitHub token must be provided via the token input.
Examples:
# Single owner by username
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
require_owner: octocat
token: ${{ secrets.GITHUB_TOKEN }}# Two owners (comma-separated)
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
require_owner: octocat,monalisa
token: ${{ secrets.GITHUB_TOKEN }}# Email addresses
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
require_owner: octocat@github.com,monalisa@example.com
token: ${{ secrets.GITHUB_TOKEN }}# Mixed usernames and emails
- uses: lfreleng-actions/tag-validate-action@v1
with:
require_signed: gpg
require_owner: octocat,monalisa@example.com
token: ${{ secrets.GITHUB_TOKEN }}Note: When require_owner is specified, require_github is implied and
does not need to be set separately.
When set to true, the action will reject tags identified as development or
pre-release versions.
Development tags are identified by the presence of keywords in the tag name:
alpha,beta,rc(release candidate)dev,pre,previewsnapshot,nightly,canary
Use cases:
- Prevent accidental releases from development tags
- Enforce production deployments
- Skip CD pipelines for pre-release versions
Examples:
# Reject development tags in production deployment
- name: "Check production tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: semver
reject_development: true# Allow development tags (default behavior)
- name: "Check any tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: semver
reject_development: false # or omit this lineDevelopment tag examples that will be rejected:
v1.0.0-alphav2.1.0-beta.1v3.0.0-rc12024.01.15-devv1.2.3-snapshot
Production tag examples that will pass:
v1.0.0v2.1.02024.01.15
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)
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 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"Enable comprehensive debug output in action logs for troubleshooting.
When enabled, the action will output:
- Bash command tracing: Shows all shell commands being executed (
set -x) - Python verbose logging: Enables DEBUG level logging from the Python CLI (
--verbose) - Internal variable values
- Git command outputs and error messages
- Tag object inspection details
- Signature verification details
- API calls and responses
- Repository cloning and tag fetching operations
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| Name | Description |
|---|---|
| valid | Set to true if tag passes all validation checks |
| tag_type | Detected tag type: semver, calver, both, or other |
| 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 |
When using the Python CLI (tag-validate), the following exit codes are returned:
| Exit Code | Name | Description |
|---|---|---|
| 0 | EXIT_SUCCESS | Validation passed |
| 1 | EXIT_VALIDATION_FAILED | Validation failed (type mismatch, signature requirements not met, etc.) |
| 2 | EXIT_MISSING_TOKEN | GitHub token required but not provided (when using --require-github) |
| 3 | EXIT_INVALID_INPUT | Invalid input parameters or malformed arguments |
| 4 | EXIT_UNEXPECTED_ERROR | Unexpected error during execution |
| 5 | EXIT_MISSING_CREDENTIALS | Gerrit credentials required but not provided (when using --require-gerrit) |
| 6 | EXIT_AUTH_FAILED | Gerrit authentication failed (invalid username or password) |
Notes:
-
Exit code
2(EXIT_MISSING_TOKEN) is specifically returned when:--require-githubflag is used butGITHUB_TOKENenvironment variable is not set- GitHub API access is required but no authentication token is available
-
Exit code
5(EXIT_MISSING_CREDENTIALS) is returned when:--require-gerritflag is used but Gerrit credentials are not provided- Gerrit server requires authentication but
GERRIT_USERNAMEorGERRIT_PASSWORDenvironment variables are not set (and no.netrcentry exists)
-
Exit code
6(EXIT_AUTH_FAILED) is returned when:- Gerrit credentials are provided but authentication fails
- Username or HTTP password is incorrect
- Note: Gerrit requires an HTTP password (from Settings > HTTP Credentials), not your SSO/LDAP password
-
Exit code
1(EXIT_VALIDATION_FAILED) covers all validation failures including:- Version type mismatch (e.g., CalVer tag when SemVer required)
- Signature requirements not met
- Signing key not registered on GitHub/Gerrit (when
--require-githubor--require-gerritis used) - Missing username for key verification
Example handling exit codes in CI:
#!/bin/bash
tag-validate verify v1.2.3 --require-github --owner myuser
exit_code=$?
case $exit_code in
0)
echo "✅ Validation passed"
;;
1)
echo "❌ Validation failed"
exit 1
;;
2)
echo "❌ GitHub token not provided"
echo "Set GITHUB_TOKEN environment variable"
exit 1
;;
5)
echo "❌ Gerrit credentials not provided"
echo "Set GERRIT_USERNAME and GERRIT_PASSWORD environment variables"
exit 1
;;
6)
echo "❌ Gerrit authentication failed"
echo "Verify your Gerrit HTTP password (not SSO/LDAP password)"
exit 1
;;
*)
echo "❌ Unexpected error (exit code: $exit_code)"
exit 1
;;
esacTag-validate supports loading Gerrit credentials from .netrc files, following
the standard format used by curl and other tools.
Search order:
.netrcin the current directory~/.netrcin your home directory~/_netrc(Windows fallback)
Example .netrc file:
machine gerrit.onap.org login myuser password mytoken
machine gerrit.opendaylight.org login myuser password anothertoken
CLI options:
| Option | Description |
|---|---|
--no-netrc |
Disable .netrc file lookup |
--netrc-file PATH |
Use a specific .netrc file |
--netrc-optional |
Do not fail if .netrc file is missing (default) |
--netrc-required |
Require a .netrc file and fail if missing |
By default, .netrc lookup is optional (--netrc-optional): if no .netrc
file is found, the tool continues and falls back to environment variables.
Use --netrc-required to enforce that a .netrc file must be present.
When a .netrc file is present, credentials load automatically. Explicit
environment variables or CLI arguments take precedence over .netrc entries.
Credential Priority Order:
- CLI arguments (highest priority)
.netrcfile (if not disabled with--no-netrc)- Environment variables (e.g.,
GERRIT_USERNAME,GERRIT_PASSWORD)
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
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- name: "Check CalVer tag"
uses: lfreleng-actions/tag-validate-action@v1
with:
require_type: calver
require_signed: gpg,ssh- 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 }}- name: "Check version from package.json"
uses: lfreleng-actions/tag-validate-action@v1
with:
tag_string: ${{ steps.get_version.outputs.version }}
require_type: semver- 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"- 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 }}"- 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 scopeUses 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
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
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
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
| 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_signedisgpgorssh.
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 2.34 or later required for SSH signing support
- GitHub Actions runners typically have Git 2.39+
For local tag validation (tag push events):
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required to fetch all tagsFor 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 }}| 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 |
| 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 |
Important: When 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:
- Omit
require_signed(accepts any signature state) - Use
require_signed=gpg-unverifiable(accepts unverifiable GPG signatures) - 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: gpgSolution: 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)
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.
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 }}# 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# 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: calverOutput changes:
dev_version→development_tag- Added:
tag_type,version_prefix,tag_name
- tag-validate-semantic-action - SemVer validation
- tag-validate-calver-action - CalVer validation
Apache-2.0
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-allBenefits:
- ✅ 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.
This action now supports verifying cryptographic signing keys against Gerrit Code Review servers. This provides enhanced security by ensuring that authorized developers with registered keys can create valid signed tags.
For comprehensive documentation on Gerrit integration, including setup examples, server configuration, and troubleshooting, see:
Key features:
- Auto-discovery of Gerrit servers from GitHub organization names
- SSH and GPG key verification against Gerrit accounts
- Support for required account owners
- Combined GitHub + Gerrit verification
- Works with popular Gerrit instances (ONAP, OpenDaylight, Eclipse, etc.)
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