skills/local/cali-ops-github-releases/SKILL.md
Create GitHub releases following project conventions. Triggers when: user says 'release', 'create release', 'push release', 'deploy to main', 'merge to main', user merges a PR to main, or when git push to main is detected. Also triggers on mentions of: gh release, semver, version bump, changelog, release-please. Covers: config-driven (read .release.yml and execute) and fallback (gh CLI) release flows, versioning rules, tag management, and the mandatory release-on-merge convention.
npx skillsauth add renatocaliari/agent-sync-public-skills cali-ops-github-releasesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Rule: Any merge/push to main MUST include a GitHub release.
Do not wait to be asked — generate release automatically.
Merging to main, pushing to main, creating release manually, setting up automation, hotfix releases, debugging release-please.
| Section | When to consult |
|---------|------------------|
| Release Strategies + sem | Before any release |
| Versioning Rules | Before bumping version |
| Config-Driven (.release.yml) | Project has .release.yml |
| Fallback: Raw gh CLI | No config file |
| Dry-Run / Preview | Preview before executing |
| Examples | First time using skill |
| Edge Cases | Problems arise |
| Test Cases | Activation reference |
Discovery order — always check in this sequence:
.release-please-manifest.json exists → project uses release-please (see references/release-please-flow.md).release.yml exists → use Config-Driven Release belowIf sem is installed (command -v sem), enhance the release with structural analysis.
If not installed, the release proceeds normally using commit-prefix parsing.
Never block a release on sem being absent.
Check: command -v sem && echo "enhanced mode" || echo "commit-parsing mode"
| Enhancement | What sem adds | Fallback |
|-------------|-----------------|----------|
| Bump type detection | Sees signature changes, removed exports → catches breaking changes commit prefixes miss | Parse feat:/fix:/BREAKING: in commit messages |
| Rich changelog | Symbol-level diff (sem diff $LAST_TAG HEAD) alongside commit list | git log --oneline only |
| Pre-release sanity | Confirms actual symbol changes exist before tagging | Trust commit list |
Full commands and integration into Steps 2-3, 10-11, and raw Steps 3-4:
references/sem-enhancement.md
Follow Semantic Versioning 2.0.0:
| Rule | Example |
|------|---------|
| Use alpha/beta only | 0.2.0-alpha, 0.3.1-beta |
| NEVER 1.0.0 without owner confirmation | ❌ 1.0.0 → ✅ 0.9.0 |
| Patch for backwards-compatible fixes | 0.2.0 → 0.2.1 |
| Minor for new features (backwards-compatible) | 0.2.0 → 0.3.0 |
| Major for breaking changes | 0.2.0 → 1.0.0 (with approval) |
Run these BEFORE any release, regardless of which flow is used:
# 1. Check gh CLI is authenticated
gh auth status || { echo "❌ Run: gh auth login"; exit 1; }
# 2. Check clean git state — auto-commit uncommitted changes
if ! git diff --quiet HEAD; then
echo "📝 Uncommitted changes found — auto-committing before release"
git add -A
# Build a descriptive commit message
FILE_COUNT=$(git diff --cached --name-only | wc -l | tr -d ' ')
ADD_DEL=$(git diff --cached --numstat | awk '{a+=$1; d+=$2} END {printf "+%d -%d", a, d}')
if command -v sem &> /dev/null; then
SEM_OUT=$(sem diff 2>/dev/null | head -3 | tr '\n' ' ' | xargs)
fi
if [ -n "$SEM_OUT" ]; then
COMMIT_MSG="chore: ${SEM_OUT} (${FILE_COUNT} files, ${ADD_DEL})"
else
COMMIT_MSG="chore: prepare release (${FILE_COUNT} files, ${ADD_DEL})"
fi
git commit -m "${COMMIT_MSG}"
echo "✅ Changes committed: $(git rev-parse HEAD | head -c 7) — ${COMMIT_MSG}"
fi
# 3. Check we're on main/master (block feature branches)
BRANCH=$(git branch --show-current)
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
echo "❌ Not on main/master branch. Current: $BRANCH"
echo " git checkout main && git pull # switch to main first"
exit 1
fi
echo "✅ On branch: $BRANCH"
.release.yml)When a project has .release.yml, the skill reads it and executes every step.
No project-level scripts needed — the skill is the executor.
.release.yml format# Suffix appended to version numbers (e.g. 0.11.1-alpha)
# Default: "" (no suffix)
suffix: alpha
# Version files to sync when bumping
# The skill will update the version field in each file.
# Default: [] (auto-detect package.json, pyproject.toml, Cargo.toml)
version_files:
- package.json
# Command to run tests before release
# Default: "" (no tests)
test_command: npm test
# Command to run lint/typecheck before release
# Default: "" (no lint)
lint_command: npm run typecheck
The skill follows this exact flow:
Step 0: Read .release.yml
Step 1: Run Pre-Flight Checklist (gh auth + clean git)
Step 2: Detect last tag (git describe --tags)
Step 3: Parse version and bump it (preserving suffix)
Step 4: Confirm major release if applicable
Step 5: Run test_command (if defined)
Step 6: Run lint_command (if defined)
Step 7: Sync version in all version_files
Step 8: Create annotated git tag
Step 9: Push tag to GitHub
Step 10: Build changelog from commits since last tag
Step 11: Create GitHub Release (gh release create)
Step 12: Verify and report
Step 0 — Read config:
# Read .release.yml values. Defaults if missing:
SUFFIX="" # default: no suffix
VERSION_FILES="" # auto-detect
TEST_CMD=""
LINT_CMD=""
Step 2-3 — Detect & bump version:
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
RAW="${LAST_TAG#v}"
# Parse: major.minor.patch (suffix comes from .release.yml, ignore tag suffix)
IFS='-' read -r VERSION_PART _REST <<< "$RAW"
IFS='.' read -r MAJOR MINOR PATCH <<< "${VERSION_PART}"
# Bump — TYPE is patch/minor/major determined from commits
case "$TYPE" in
patch) PATCH=$((PATCH + 1)) ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
esac
# Apply suffix from .release.yml (populated by agent reading the config)
CONFIG_SUFFIX="${YML_SUFFIX:-}"
if [ -n "$CONFIG_SUFFIX" ]; then
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}-${CONFIG_SUFFIX}"
else
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
fi
NEW_TAG="v${NEW_VERSION}"
Step 4 — Major confirmation:
if [ "$TYPE" = "major" ]; then
echo "⚠ MAJOR release: $LAST_TAG → $NEW_TAG"
echo "This requires owner confirmation. Ask the user before proceeding."
fi
Step 5-6 — Run tests/lint:
if [ -n "$TEST_CMD" ]; then
echo "➜ Running tests: $TEST_CMD"
eval "$TEST_CMD" || { echo "Tests failed — aborting release"; exit 1; }
fi
if [ -n "$LINT_CMD" ]; then
echo "➜ Running lint: $LINT_CMD"
eval "$LINT_CMD" || { echo "Lint failed — aborting release"; exit 1; }
fi
Step 7 — Sync version files:
# Read version_files from .release.yml
# For each file, update the version field
for f in "${VERSION_FILES[@]}"; do
case "$f" in
*.json)
node -e "
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('./$f', 'utf8'));
if (p.version) {
p.version = '$NEW_VERSION';
fs.writeFileSync('./$f', JSON.stringify(p, null, 2) + '\n');
} else {
console.error('WARNING: no version field in $f');
}
"
git add "$f"
;;
*.toml)
sed -i.bak 's/^version = ".*"/version = "'"$NEW_VERSION"'"/' "$f" && rm -f "$f.bak"
git add "$f"
;;
*.go)
# Go version file: var/const Version = "x.y.z"
sed -i.bak 's/\(Version\\s*=\\s*\)".*"/\1"'"$NEW_VERSION"'"/' "$f" && rm -f "$f.bak"
git add "$f"
;;
esac
done
# Commit version bump BEFORE tagging (tag must point to the bumped commit)
if ! git diff --cached --quiet; then
git commit -m "chore: bump version to $NEW_VERSION"
fi
Step 8-9 — Tag + push:
git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
git push origin "$NEW_TAG"
Step 10-11 — Create GitHub Release:
Follow Keep a Changelog 1.1.0 format:
# Build changelog — handle root commit / first tag gracefully
PREV_TAG=$(git describe --tags --abbrev=0 "$NEW_TAG"^ 2>/dev/null || true)
CATEGORIZED=$(mktemp)
CHANGELOG=$(mktemp)
# Categorize commits by conventional commit prefix
if [ -n "$PREV_TAG" ]; then
LOG_RANGE="$PREV_TAG..$NEW_TAG"
SCOPE_LABEL="$PREV_TAG"
else
LOG_RANGE="HEAD"
SCOPE_LABEL="initial commit"
fi
# Build categorized changelog following Keep a Changelog format
{
echo "# Release $NEW_TAG"
echo ""
echo "## What's Changed"
echo ""
# Added (feat:)
ADDED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^feat" 2>/dev/null | sed 's/^/* /')
if [ -n "$ADDED" ]; then
echo "### Added"
echo "$ADDED"
echo ""
fi
# Fixed (fix:)
FIXED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^fix" 2>/dev/null | sed 's/^/* /')
if [ -n "$FIXED" ]; then
echo "### Fixed"
echo "$FIXED"
echo ""
fi
# Changed (refactor:, perf:, chore:)
CHANGED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^refactor\|^perf\|^chore" 2>/dev/null | sed 's/^/* /')
if [ -n "$CHANGED" ]; then
echo "### Changed"
echo "$CHANGED"
echo ""
fi
# Security (security:)
SECURITY=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^security\|^sec" 2>/dev/null | sed 's/^/* /')
if [ -n "$SECURITY" ]; then
echo "### Security"
echo "$SECURITY"
echo ""
fi
# Remaining uncategorized commits
CATEGORIZED_COM=$( (git log "$LOG_RANGE" --oneline --no-decorate --grep="^feat\|^fix\|^refactor\|^perf\|^chore\|^security\|^sec" 2>/dev/null) | wc -l | tr -d ' ')
TOTAL_COM=$(git log "$LOG_RANGE" --oneline --no-decorate 2>/dev/null | wc -l | tr -d ' ')
if [ "$TOTAL_COM" -gt "$CATEGORIZED_COM" ]; then
echo "### Other"
git log "$LOG_RANGE" --oneline --no-decorate --invert-grep --grep="^feat\|^fix\|^refactor\|^perf\|^chore\|^security\|^sec" 2>/dev/null | sed 's/^/* /'
echo ""
fi
echo "---"
echo "_Full diff: [$SCOPE_LABEL...$NEW_TAG](https://github.com/$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || echo 'repo')/compare/$SCOPE_LABEL...$NEW_TAG)_"
} > "$CATEGORIZED"
# Use --notes-file to avoid shell escaping bugs
gh release create "$NEW_TAG" --title "$NEW_TAG" --notes-file "$CATEGORIZED"
rm -f "$CATEGORIZED" "$CHANGELOG"
Use this when the project has neither .release-please-manifest.json nor .release.yml.
# Execute the checks in [Pre-Flight Checklist](#pre-flight-checklist-all-flows)
# If any check fails, stop and tell the user what to fix.
# Find last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
echo "No previous tags found. This is the first release."
# Check for existing version indicators
cat package.json 2>/dev/null | grep '"version"' || true
grep 'version =' Cargo.toml 2>/dev/null || true
else
echo "Last tag: $LAST_TAG"
echo ""
echo "Commits since last release:"
git log "$LAST_TAG"..HEAD --oneline --no-decorate
fi
Based on the commits found:
fix: commits → patch bump
feat: commits → minor bump
BREAKING CHANGE: or !: → major bump (requires user confirmation)
refactor: / chore: / docs: → patch bump (safe default)
Mixed feat + fix → minor bump
No conventional commit prefixes → ask the user with this template:
"The commits don't follow conventional commit format (no
feat:/fix:prefixes). Here are the changes since the last release:[paste git log output]What kind of version bump should this be?
patch(fixes) /minor(features) /major(breaking)"
# Compute next version
RAW="${LAST_TAG#v}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$RAW"
# Bump based on decision above
NEW_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" # default: patch
echo "Proposed: $LAST_TAG → $NEW_TAG"
# Build categorized changelog following Keep a Changelog format
CHANGELOG=$(mktemp)
if [ -n "$LAST_TAG" ]; then
LOG_RANGE="$LAST_TAG..HEAD"
else
LOG_RANGE="HEAD"
fi
{
echo "# Release $NEW_TAG"
echo ""
echo "## What's Changed"
echo ""
# Added (feat:)
ADDED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^feat" 2>/dev/null | sed 's/^/* /')
if [ -n "$ADDED" ]; then
echo "### Added"
echo "$ADDED"
echo ""
fi
# Fixed (fix:)
FIXED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^fix" 2>/dev/null | sed 's/^/* /')
if [ -n "$FIXED" ]; then
echo "### Fixed"
echo "$FIXED"
echo ""
fi
# Changed (refactor:, perf:, chore:)
CHANGED=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^refactor\|^perf\|^chore" 2>/dev/null | sed 's/^/* /')
if [ -n "$CHANGED" ]; then
echo "### Changed"
echo "$CHANGED"
echo ""
fi
# Security (security:)
SECURITY=$(git log "$LOG_RANGE" --oneline --no-decorate --grep="^security\|^sec" 2>/dev/null | sed 's/^/* /')
if [ -n "$SECURITY" ]; then
echo "### Security"
echo "$SECURITY"
echo ""
fi
# Remaining uncategorized commits
CATEGORIZED_COM=$( (git log "$LOG_RANGE" --oneline --no-decorate --grep="^feat\|^fix\|^refactor\|^perf\|^chore\|^security\|^sec" 2>/dev/null) | wc -l | tr -d ' ')
TOTAL_COM=$(git log "$LOG_RANGE" --oneline --no-decorate 2>/dev/null | wc -l | tr -d ' ')
if [ "$TOTAL_COM" -gt "$CATEGORIZED_COM" ]; then
echo "### Other"
git log "$LOG_RANGE" --oneline --no-decorate --invert-grep --grep="^feat\|^fix\|^refactor\|^perf\|^chore\|^security\|^sec" 2>/dev/null | sed 's/^/* /'
echo ""
fi
} > "$CHANGELOG"
# Create the release
gh release create "$NEW_TAG" \
--title "$NEW_TAG" \
--notes-file "$CHANGELOG"
rm -f "$CHANGELOG"
gh release view "$NEW_TAG"
echo "✅ Release $NEW_TAG created"
Input: "Just merged the auth feature to main"
Discovery: .release.yml exists → use config-driven flow
Execution:
.release.yml: suffix=alpha, test_command=npm test, version_files=[package.json]v0.2.0-alphav0.3.0-alpha (feature = minor)npm test ✅package.json → 0.3.0-alphav0.3.0-alphaOutput: "Released v0.3.0-alpha. Tests passed. Version synced to package.json."
Input: "Release this"
Discovery: No .release.yml, no release-please → fallback to raw gh CLI
Execution:
v0.1.0feat: add login, fix: button colorv0.2.0gh release create v0.2.0 --title "v0.2.0" --notes "..."Output: "Released v0.2.0"
Input: "Production is down, critical bug in auth"
Execution:
git checkout -b hotfix/fix-auth-crash main.release.yml exists, raw gh CLI otherwise)Output: "Hotfix released. Production should recover."
To preview what would happen without making changes, the agent should:
sem available, run sem diff to enhance the previewWould create tag vX.Y.Z from branch <branch> with N commitssem diff if available)Example output:
[Dry-Run] sem: 3 symbols changed (1 new function, 2 modified)
[Dry-Run] Would bump v0.2.0-alpha → v0.3.0-alpha (minor: 2 feat commits)
[Dry-Run] Changelog:
feat: add login page
feat: add dashboard
fix: button hover color
[Dry-Run] No actions taken. Run without --dry-run to execute.
go test ./... or npm testgit diff v0.X.Y-alpha..HEAD --statgh auth statusgh auth logingit tag -l "v0.X.Y*"sem is installed: chore: <sem diff summary> (N files, +M -K)chore: prepare release (N files, +M -K)git reset --soft HEAD~1git tag -s instead of -a)tag.gpgSign = true in git config, use -s flagerror: gpg failed to sign the taggit push origin "$NEW_TAG" is rejected: "Tag push rejected — main branch may be protected"mainpackages/*), .release.yml needs version_files per packagepkg-a-v0.3.0, pkg-b-v0.1.5)feat:, fix:, etc.)refactor: commits don't trigger releases — use manual flow.release-please-manifest.json existsreferences/release-please-flow.mdError reading JToken from JsonReader or syntax errorsjqreferences/release-please-flow.mdv0.1.0 (or v0.1.0-alpha with suffix)references/release-please-flow.md — release-please automation, refactor handling, JSON safety, configreferences/sem-enhancement.md — sem diff: bump detection, rich changelog, sanity checkcali-ops-deploy-github-tailscale — deploy pipeline (trigger after release)development
PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning.
tools
Auto-initialize structured documentation for any project using lat.md (knowledge graph of markdown files with [[wiki links]], // @lat: code refs, and semantic search). Detects cali-product-workflow artifacts (spec-product.md, spec-tech.md, critiques) and uses them as seed material. Falls back to extracting business rules, architecture, and design decisions directly from the codebase. Use when a project lacks structured documentation or when lat.md/ is missing. After seeding, lat.md extension hooks keep documentation alive automatically.
testing
[Cali] Server security audit and hardening for private servers behind Tailscale. Use when: auditing server security, hardening SSH/firewall/Docker, checking for vulnerabilities, setting up fail2ban, reviewing port exposure, or responding to security alerts. Covers 6 layers: CloudFlare, UFW, Tailscale, SSH, Docker, Application. Triggers: "server security", "security audit", "harden server", "SSH hardening", "firewall rules", "UFW config", "fail2ban", "port security", "Docker security", "vulnerability check", "security review".
tools
Run supply chain security scans before installing packages or before releases. Triggers when: user installs a package (npm, pip, go get, brew), user asks to 'scan dependencies', 'check vulnerabilities', 'supply chain', 'security audit', 'run trivy', 'run socket', or before any release/deployment. Also triggers on mentions of: socket.dev, trivy, OSV-scanner, dotenvx, CVE, dependency audit. Covers all four tools with concrete commands.