From f9ed59786fc194f1681aa3e1bc8e28c722737d69 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 21 Jun 2026 14:38:59 +0530 Subject: [PATCH] feat: Automate versioning workflow with release script and semantic rules --- .agents/skills/release/SKILL.md | 40 ++++++++++++++++++++ VERSION | 2 +- bootstrap.sh | 2 +- commands/up.sh | 16 -------- docs/versioning.md | 46 +++++++++++++++++++++++ lib/common.sh | 17 +++++++++ scripts/release.sh | 65 +++++++++++++++++++++++++++++++++ 7 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 .agents/skills/release/SKILL.md create mode 100644 docs/versioning.md create mode 100755 scripts/release.sh diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 0000000..e15bfab --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,40 @@ +--- +name: release +description: > + Cut a new release of the bootstrap CLI. Use this skill when the user asks + to bump the version, tag a release, or cut a new version. Analyzes recent + commits to determine the appropriate semver bump level and runs + scripts/release.sh automatically. +--- + +# Release Skill + +This skill automates the versioning and release process for the bootstrap CLI. + +## Workflow + +When the user asks to "cut a release", "bump the version", or "tag a new version": + +1. **Analyze commits since the last tag**: + ```bash + git log $(git describe --tags --abbrev=0)..HEAD --oneline + ``` +2. **Determine the bump level** using semantic versioning rules: + - Skip any commits that only touch `installers/` or docs — those don't warrant a bump. + - If any commit has `BREAKING CHANGE` or `!:` → **major** + - If any core-CLI commit has `feat:` → **minor** + - Otherwise (only `fix:`, `refactor:`, etc. in core-CLI) → **patch** + - If *all* commits are installer-only or docs-only → inform the user no release is needed. +3. **Run the release script** non-interactively (if an actual bump is needed): + ```bash + ./scripts/release.sh -- -y + ``` + (e.g., `./scripts/release.sh --minor -y`) +4. **Push** the tag and commit (ask for user confirmation before pushing): + ```bash + git push origin master + ``` + +## Dry-run Mode + +If the user asks "what would the next version be?" or similar, do the analysis (steps 1 and 2) but do NOT run the release script. Just report the recommended bump level and why. diff --git a/VERSION b/VERSION index 512a1fa..5ed5faa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.9 +1.1.10 diff --git a/bootstrap.sh b/bootstrap.sh index be91df5..e6349da 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -239,7 +239,7 @@ EOF _version=$(cat "$_version_file" | tr -d '\r\n') fi if [ -z "$_version" ]; then - _version="1.0.7" # Fallback matching VERSION file + _version="0.0.0" # Fallback if VERSION missing fi _version_str="v$_version" _version_len=${#_version_str} diff --git a/commands/up.sh b/commands/up.sh index cbeeaa0..50c7cbb 100644 --- a/commands/up.sh +++ b/commands/up.sh @@ -26,22 +26,6 @@ if [ -z "$remote_ver" ]; then exit 1 fi -# Version comparison helper -version_lt() { - [ "$1" = "$2" ] && return 1 - local IFS=. - local i ver1=($1) ver2=($2) - for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done - for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done - for ((i=0; i<3; i++)); do - if ((10#${ver1[i]} < 10#${ver2[i]})); then - return 0 - elif ((10#${ver1[i]} > 10#${ver2[i]})); then - return 1 - fi - done - return 1 -} log_info "Local version: $local_ver" log_info "Remote version: $remote_ver" diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 0000000..fd1a5c1 --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,46 @@ +# Versioning Workflow + +The Bootstrap CLI uses a semantic, git-tag-driven versioning workflow. + +## 1. During Normal Development + +- **Do not manually edit the `VERSION` file.** +- Develop normally using [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: Add new tool`, `fix: Typo in script`). +- **Note on Installers:** Adding or modifying installers in the `installers/` directory **does not** require a version bump. They are fetched dynamically from the registry when a user runs `b ware `. + +## 2. When to Cut a Release + +You should cut a new release when you have accumulated enough changes in the core CLI files (e.g., `bootstrap.sh`, `b.sh`, or scripts in `commands/` and `lib/`). + +### Semantic Versioning Rules + +- **Patch (`x.x.Z`)**: Bug fixes and internal refactors (e.g., `fix:` or `refactor:`). +- **Minor (`x.Y.0`)**: New core features or commands (e.g., `feat:`). +- **Major (`X.0.0`)**: Breaking changes to user workflows (e.g., renaming core commands, breaking backward compatibility). + +## 3. How to Cut a Release + +You can cut a release automatically using the release script: + +```bash +./scripts/release.sh +``` + +Alternatively, if you are using an AI agent, you can just tell it: *"cut a release"*. + +### What the Release Script Does + +1. Prompts you to select the bump level (Patch, Minor, Major). If using an agent, it will automatically analyze your recent commit history and decide the appropriate level based on the conventional commit prefixes. +2. Bumps the version number (e.g., `v1.1.9` -> `v1.2.0`). +3. Writes the new version into the `VERSION` file and runs `git commit`. +4. Creates an annotated git tag (e.g., `git tag -a v1.2.0`). + +## 4. How Users Receive Updates + +Once you push the new tag and commit to the `master` branch: + +```bash +git push origin master v1.2.0 +``` + +The release goes live immediately. Whenever users run any `b` command, `b.sh` performs a background check against the raw `VERSION` file on the `master` branch. Because the release process automatically updates this file, the CLI detects the new version and downloads the updated files dynamically. diff --git a/lib/common.sh b/lib/common.sh index 4ce214e..bedd540 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -67,3 +67,20 @@ make_temp_dir() { tmp_dir="$(mktemp -d)" echo "$tmp_dir" } + +# Version comparison helper (returns 0 if $1 < $2, 1 otherwise) +version_lt() { + [ "$1" = "$2" ] && return 1 + local IFS=. + local i ver1=($1) ver2=($2) + for ((i=${#ver1[@]}; i<3; i++)); do ver1[i]=0; done + for ((i=${#ver2[@]}; i<3; i++)); do ver2[i]=0; done + for ((i=0; i<3; i++)); do + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 0 + elif ((10#${ver1[i]} > 10#${ver2[i]})); then + return 1 + fi + done + return 1 +} diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..ddc9511 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# scripts/release.sh — Tag a new release +# Usage: +# Interactive: ./scripts/release.sh +# Non-interactive: ./scripts/release.sh --patch|--minor|--major [-y] +set -euo pipefail + +current=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") +IFS='.' read -r cur_major cur_minor cur_patch <<< "${current#v}" + +bump="" +auto_confirm=false + +# Parse flags for non-interactive (agent) usage +while [[ $# -gt 0 ]]; do + case "$1" in + --patch) bump="patch"; shift ;; + --minor) bump="minor"; shift ;; + --major) bump="major"; shift ;; + -y|--yes) auto_confirm=true; shift ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +echo "Current version: $current" + +# Interactive fallback if no flag provided +if [ -z "$bump" ]; then + echo "" + echo "What type of release?" + echo " 1) patch (bug fixes, internal changes)" + echo " 2) minor (new features, new commands)" + echo " 3) major (breaking changes)" + read -rp "Choice [1/2/3]: " choice + case "$choice" in + 1) bump="patch" ;; + 2) bump="minor" ;; + 3) bump="major" ;; + *) echo "Invalid choice"; exit 1 ;; + esac +fi + +case "$bump" in + patch) cur_patch=$((cur_patch + 1)) ;; + minor) cur_minor=$((cur_minor + 1)); cur_patch=0 ;; + major) cur_major=$((cur_major + 1)); cur_minor=0; cur_patch=0 ;; +esac + +new_ver="v${cur_major}.${cur_minor}.${cur_patch}" + +if [ "$auto_confirm" = true ]; then + confirm="y" +else + read -rp "Tag as $new_ver? [y/N]: " confirm +fi + +if [[ "$confirm" =~ ^[Yy]$ ]]; then + echo "${new_ver#v}" > VERSION + git add VERSION + git commit -m "release: $new_ver" + git tag -a "$new_ver" -m "Release $new_ver" + echo "Tagged $new_ver. Push with: git push origin master $new_ver" +else + echo "Aborted." +fi