| name | skill-tag |
| description | Create and push semantic version tags for CI/CD deployment. User-only command - agents cannot invoke. |
| allowed-tools | Bash, AskUserQuestion, Read |
| user-only | true |
Tag Skill (Direct Execution)
Direct execution skill for creating and pushing semantic version tags to trigger CI/CD deployment. This command is user-only - agents MUST NOT invoke it.
CRITICAL: Deployment timing is user-controlled. This command creates and pushes git tags which immediately trigger CI/CD deployment to production.
Command Syntax
/tag [--patch|--minor|--major] [--force] [--dry-run] [--skip-version-check]
| Flag | Description |
|---|
--patch | Increment patch version (default): v0.2.3 -> v0.2.4 |
--minor | Increment minor version, reset patch: v0.2.3 -> v0.3.0 |
--major | Increment major version, reset minor and patch: v0.2.3 -> v1.0.0 |
--force | Skip confirmation prompt |
--dry-run | Show what would be done without executing |
--skip-version-check | Bypass a declared-version mismatch (still prints a loud warning naming both versions) |
Execution
Step 1: Parse Arguments
Extract flags from command input:
increment="patch"
force=false
dry_run=false
skip_version_check=false
if [[ "$*" == *"--minor"* ]]; then
increment="minor"
fi
if [[ "$*" == *"--major"* ]]; then
increment="major"
fi
if [[ "$*" == *"--force"* ]]; then
force=true
fi
if [[ "$*" == *"--dry-run"* ]]; then
dry_run=true
fi
if [[ "$*" == *"--skip-version-check"* ]]; then
skip_version_check=true
fi
Step 2: Validate Git State
Check that the repository is in a valid state for tagging:
echo "=== Validating Git State ==="
echo ""
if [ -n "$(git status --porcelain)" ]; then
echo "Error: Working tree has uncommitted changes."
echo ""
echo "Uncommitted files:"
git status --short
echo ""
echo "Resolution: Commit or stash changes before tagging."
exit 1
fi
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" = "HEAD" ]; then
echo "Error: Detached HEAD state. Checkout a branch before tagging."
exit 1
fi
git fetch origin "$current_branch" --quiet 2>/dev/null || true
local_sha=$(git rev-parse HEAD)
remote_sha=$(git rev-parse "origin/$current_branch" 2>/dev/null || echo "")
if [ -n "$remote_sha" ] && [ "$local_sha" != "$remote_sha" ]; then
behind=$(git rev-list --count "HEAD..origin/$current_branch" 2>/dev/null || echo "0")
if [ "$behind" -gt 0 ]; then
echo "Error: Local branch is $behind commit(s) behind remote."
echo ""
echo "Resolution: Pull latest changes with 'git pull' before tagging."
exit 1
fi
fi
echo "Git state: OK (clean working tree, up-to-date with remote)"
Step 3: Compute New Version
Get current version and compute new version based on increment type:
echo ""
echo "=== Computing Version ==="
echo ""
current_version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Current version: $current_version"
major=$(echo "$current_version" | sed 's/v\([0-9]*\).*/\1/')
minor=$(echo "$current_version" | sed 's/v[0-9]*\.\([0-9]*\).*/\1/')
patch=$(echo "$current_version" | sed 's/v[0-9]*\.[0-9]*\.\([0-9]*\)/\1/')
case "$increment" in
patch)
patch=$((patch + 1))
;;
minor)
minor=$((minor + 1))
patch=0
;;
major)
major=$((major + 1))
minor=0
patch=0
;;
esac
new_version="v${major}.${minor}.${patch}"
echo "New version: $new_version ($increment release)"
if git rev-parse "$new_version" >/dev/null 2>&1; then
echo ""
echo "Error: Tag $new_version already exists."
echo ""
echo "Existing tags:"
git tag -l 'v*' --sort=-v:refname | head -5
echo ""
echo "Resolution: Use a different increment type or check tag history."
exit 1
fi
Step 3.5: Validate Version Consistency
Ensure any declared package version agrees with the computed tag before proceeding. This step
runs for every invocation path — default, --force, and --dry-run alike — because it sits
strictly before Step 5's --dry-run exit 0:
echo ""
echo "=== Validating Version Consistency ==="
echo ""
new_version_bare="${new_version#v}"
repo_root=$(git rev-parse --show-toplevel)
manifest_files=$(find "$repo_root" -maxdepth 3 \
-not -path '*/node_modules/*' \
-not -path '*/.git/*' \
-not -path '*/dist/*' \
-not -path '*/build/*' \
-not -path '*/target/*' \
-not -path '*/__pycache__/*' \
-not -path '*/.venv/*' \
-not -path '*/venv/*' \
\( -name pyproject.toml -o -name setup.cfg -o -name setup.py -o -name package.json -o -name Cargo.toml \) \
2>/dev/null | sort)
extract_declared_version() {
local file="$1"
local base
base=$(basename "$file")
case "$base" in
pyproject.toml)
sed -n '/^\[project\]/,/^\[/p' "$file" \
| grep -m1 '^version[[:space:]]*=' \
| sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/'
;;
setup.cfg)
sed -n '/^\[metadata\]/,/^\[/p' "$file" \
| grep -m1 '^version[[:space:]]*=' \
| sed -E 's/^version[[:space:]]*=[[:space:]]*([^ ]*).*/\1/'
;;
setup.py)
grep -m1 -E "version[[:space:]]*=[[:space:]]*['\"]" "$file" \
| sed -E "s/.*version[[:space:]]*=[[:space:]]*['\"]([^'\"]*)['\"].*/\1/"
;;
package.json)
jq -r '.version // empty' "$file"
;;
Cargo.toml)
sed -n '/^\[package\]/,/^\[/p' "$file" \
| grep -m1 '^version[[:space:]]*=' \
| sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/'
;;
esac
}
declared_pairs=""
while IFS= read -r manifest; do
[ -z "$manifest" ] && continue
declared_version=$(extract_declared_version "$manifest")
if [ -n "$declared_version" ]; then
declared_pairs="${declared_pairs}${manifest}:${declared_version}"$'\n'
fi
done <<< "$manifest_files"
declared_pairs="${declared_pairs%$'\n'}"
if [ -z "$declared_pairs" ]; then
echo "No declared package version found (checked pyproject.toml, setup.cfg, setup.py,"
echo "package.json, Cargo.toml near repo root). Skipping version-consistency check."
else
mismatch_found=false
while IFS=: read -r manifest declared_version; do
[ -z "$manifest" ] && continue
declared_bare="${declared_version#v}"
if [ "$declared_bare" != "$new_version_bare" ]; then
mismatch_found=true
fi
done <<< "$declared_pairs"
if [ "$mismatch_found" = true ]; then
echo "Error: Declared package version does not match computed tag ($new_version)."
echo ""
echo "Declared in:"
while IFS=: read -r manifest declared_version; do
[ -z "$manifest" ] && continue
echo " $manifest (version = \"$declared_version\")"
done <<< "$declared_pairs"
echo ""
if [ "$skip_version_check" = true ]; then
echo "WARNING: --skip-version-check is set. Proceeding despite the mismatch above."
echo "Computed tag: $new_version. The declared version(s) listed above diverge from it."
else
echo "Resolution: Update the mismatched manifest(s) to $new_version_bare, commit, then re-run /tag."
echo "Or pass --skip-version-check to proceed anyway (not recommended)."
exit 1
fi
else
echo "Version consistency: OK -- declared version(s) match computed tag ($new_version)."
while IFS=: read -r manifest declared_version; do
[ -z "$manifest" ] && continue
echo " $manifest (version = \"$declared_version\")"
done <<< "$declared_pairs"
fi
fi
Step 4: Display Summary
Show what will be deployed:
echo ""
echo "=== Deployment Summary ==="
echo ""
echo "Version: $current_version -> $new_version"
echo "Branch: $current_branch"
echo "Commit: $(git rev-parse --short HEAD)"
echo ""
commits_since=$(git log "$current_version"..HEAD --oneline 2>/dev/null | wc -l)
if [ "$commits_since" -gt 0 ]; then
echo "Commits since $current_version ($commits_since total):"
git log "$current_version"..HEAD --oneline | head -10
if [ "$commits_since" -gt 10 ]; then
echo " ... and $((commits_since - 10)) more"
fi
else
echo "No commits since $current_version"
fi
echo ""
echo "This will trigger CI/CD deployment to production."
Step 5: Execute Based on Mode