dsc-rs 0.10.30

Discourse CLI tool for managing multiple Discourse forums: track installs, run upgrades over SSH, manage emojis, sync topics and categories as Markdown, and more.
Documentation
#!/usr/bin/env bash
#
# Usage: s/version++ [patch|minor|major]   (default: patch)
#
# The one release action. In order: bumps the version in Cargo.toml,
# regenerates CHANGELOG.md (git-cliff), commits the release, tags it, and
# pushes main + the tag. The pushed tag triggers the cargo-dist release
# workflow (prebuilt binaries + GitHub Release) and the crates.io publish.
#
# Commit your feature work FIRST, with a conventional-commit message
# (`feat(...)`, `fix:`, ...). git-cliff reads *committed* history, so anything
# uncommitted won't appear in the new CHANGELOG.md section; this script makes
# only the release commit.
#
# Note on tagging (divergence from ~/code/house-style/distribution.md):
# the house standard is s/version++ commits+pushes and a CI auto-tag.yml
# creates the tag via `workflow_call`. That needs a workflow_call-able
# release workflow. dsc's release.yml is cargo-dist-generated and is
# *tag-driven* (it reads the version from the pushed tag ref and only
# triggers on `push: tags`), and there's a second tag-triggered workflow
# (publish-crates.yml). So the cascade doesn't fit here without a PAT
# secret to push the tag from CI. Until then we tag locally, AFTER the
# commit (fixing the tag-before-commit hazard the standard warns about);
# with no auto-tag there's no duplicate-tag race.

set -euo pipefail
cd "$(git rev-parse --show-toplevel)"

bump="${1:-patch}"
case "$bump" in
  patch | minor | major) ;;
  *)
    echo "Usage: s/version++ [patch|minor|major]" >&2
    exit 2
    ;;
esac

# Release from main only.
branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$branch" != "main" ]]; then
  echo "On '$branch', not 'main'. Releases are cut from main." >&2
  exit 1
fi

# The release commit must contain only the bump + changelog, so the tree must
# be clean (feature work already committed).
if ! git diff --quiet || ! git diff --cached --quiet; then
  echo "Working tree not clean. Commit your feature work first, then release." >&2
  exit 1
fi

if ! command -v git-cliff >/dev/null 2>&1; then
  echo "git-cliff is required to refresh CHANGELOG.md but isn't installed." >&2
  echo "Install with: cargo install git-cliff --locked" >&2
  exit 1
fi

# Pre-release gate: the shared fmt + clippy + full-test-suite check (which itself
# mirrors .github/workflows/ci.yml), so we never cut and push a release that CI
# would then reject *after* the tag has already triggered the irreversible
# release. Runs before anything is modified, so a failure aborts cleanly.
echo "Pre-release checks (mirror CI): fmt + clippy + full test suite..."
s/test-fmt-clippy

cargo set-version --bump "$bump"
version="$(awk -F\" '/^version = / {print $2; exit}' Cargo.toml)"
tag="v$version"

if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
  echo "Tag $tag already exists." >&2
  exit 1
fi

# Render the in-progress changelog section under the new tag + date.
git-cliff --tag "$tag" -o CHANGELOG.md

# Sync Cargo.lock's own version to the bump (workspace members only, external
# deps untouched), then commit the release and tag it AFTER the commit (so the
# tag contains the bump).
cargo update --workspace >/dev/null 2>&1 || true
git add Cargo.toml Cargo.lock CHANGELOG.md
git commit -m "chore(release): $tag"
git tag -a "$tag" -m "Release $tag"

git push origin main
git push origin "$tag"

echo
echo "Released $tag. CI is building binaries + publishing to crates.io."