Skip to content

Instantly share code, notes, and snippets.

@paperscissors
Created March 12, 2026 11:20
Show Gist options
  • Select an option

  • Save paperscissors/b3696e61cc1e56d6a429aeeddd1c1d26 to your computer and use it in GitHub Desktop.

Select an option

Save paperscissors/b3696e61cc1e56d6a429aeeddd1c1d26 to your computer and use it in GitHub Desktop.
Streamline git releases; detect if we're in a JS project and also bump up the package.json version to match the passed version
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
BOLD=''
RESET=''
fi
info() { printf "%b\n" "${BLUE}${BOLD}INFO${RESET} $*"; }
warn() { printf "%b\n" "${YELLOW}${BOLD}WARN${RESET} $*"; }
error() { printf "%b\n" "${RED}${BOLD}ERROR${RESET} $*"; }
success() { printf "%b\n" "${GREEN}${BOLD}OK${RESET} $*"; }
die() { error "$*"; exit 1; }
usage() {
cat <<'EOF'
Usage: git-release VERSION
Example: git-release 0.9.58
No VERSION: prints current package.json version (if present).
EOF
}
if [[ $# -eq 0 ]]; then
if [[ ! -f package.json ]]; then
usage
exit 1
fi
if ! python3 - <<'PY'
import re, pathlib, sys
text = pathlib.Path("package.json").read_text()
m = re.search(r'^\s*"version"\s*:\s*"([^"]+)"', text, re.M)
if not m:
sys.exit(2)
print(m.group(1))
PY
then
die "package.json does not contain a top-level version field."
fi
exit 0
fi
if [[ $# -ne 1 ]]; then
usage
exit 1
fi
VERSION="$1"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
die "Not inside a git repository."
fi
if [[ -n "$(git status --porcelain)" ]]; then
error "Working tree is not clean. Commit or stash changes first."
git status --short
exit 1
fi
if [[ ! -f package.json ]]; then
die "package.json not found in $(pwd)."
fi
if ! CURRENT_VERSION="$(python3 - <<'PY'
import re, pathlib, sys
text = pathlib.Path("package.json").read_text()
m = re.search(r'^\s*"version"\s*:\s*"([^"]+)"', text, re.M)
if not m:
sys.exit(2)
print(m.group(1))
PY
)"; then
die "package.json does not contain a top-level version field."
fi
if [[ "$CURRENT_VERSION" == "$VERSION" ]]; then
warn "package.json already at version $VERSION. Skipping version bump."
else
info "Updating package.json version: $CURRENT_VERSION -> $VERSION"
if ! python3 - <<'PY' "$VERSION"
import re, pathlib, sys
target = sys.argv[1]
path = pathlib.Path("package.json")
text = path.read_text()
pattern = r'^(\s*"version"\s*:\s*")([^"]+)(")'
m = re.search(pattern, text, re.M)
if not m:
sys.exit(2)
new_text = re.sub(pattern, lambda m: m.group(1) + target + m.group(3), text, count=1, flags=re.M)
path.write_text(new_text)
PY
then
die "Failed to update package.json version."
fi
git add package.json
git commit -m "chore: bump version to v$VERSION"
success "Committed version bump."
fi
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
die "Tag v$VERSION already exists."
fi
if ! git remote get-url origin >/dev/null 2>&1; then
die "Remote 'origin' not found."
fi
if ! git show-ref --verify --quiet refs/heads/main; then
die "Local branch 'main' not found."
fi
info "Creating annotated tag v$VERSION"
git tag -a "v$VERSION" -m "v$VERSION"
info "Pushing main and tags to origin"
git push origin main --follow-tags
success "Release v$VERSION complete."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment