CLI
Release tools for bash workflows - a comprehensive toolkit of bash scripts for various CI/CD purposes including version management, release automation, and workflow utilities.
This toolkit represents a collection of bash scripts for various purposes. At the time of this writing, this is a work in progress, with more utilities being added as scripts are centralized from various repositories.
Any and all contributions are welcome; just open a PR.
Quickstart
1. Install the tools
Choose your preferred installation method:
Using curl
bash <(curl -sSL "https://github.com/releasetools/cli/releases/download/v0.0.12/install.sh")
Using wget
bash <(wget -q -O- "https://github.com/releasetools/cli/releases/download/v0.0.12/install.sh")
Using Homebrew
brew tap releasetools/tap
brew install releasetools-cli
With curl or wget, the tools are installed to ~/.local/share/releasetools/cli/VERSION/ and a binary is symlinked at ~/.local/bin/releasetools.
rt shorthandThe Homebrew formula additionally symlinks releasetools to rt, so rt version works
after brew install. That alias is specific to Homebrew — the curl, wget and GitHub
Action installs provide only releasetools. This page uses the long name throughout,
since it works for every installation method.
2. Utilize the releasetools library
# With ~/.local/bin in your PATH:
export PATH=~/.local/bin:"$PATH"
# You can run commands, e.g. (the installers also link the shorter name 'rt'):
releasetools version
# vX.Y.Z
# Optionally, check that all dependencies for all modules are correctly installed
releasetools base::check_deps
# Ok.
# You can also check the install location
releasetools base::install_location
# /Users/user/.local/share/releasetools/cli/vX.Y.Z/releasetools.bash
Customizations
Several customizations can be applied prior to installation:
1. Installation Directory
Customize the location where the tools will be installed:
export RELEASETOOLS_INSTALL_DIR="$HOME/.local/share"
# proceed with the installation steps outlined above
2. Binary Location
Customize the path where the binary is symlinked:
export RELEASETOOLS_BINARY_DIR="$HOME/.local/bin"
# proceed with the installation steps outlined above
GitHub Action
The releasetools/cli library can be installed via a GitHub workflow:
Basic Usage
steps:
# Install releasetools
- uses: releasetools/cli@v0
# Check that releasetools was installed correctly
- run: releasetools base::check_deps
Advanced Configuration
A few customizations are available, if needed:
steps:
# Install releasetools with customizations
- uses: releasetools/cli@v0
with:
# Pin a specific version (defaults to latest)
version: "v0.0.12"
env:
# Configure the installation directory
RELEASETOOLS_INSTALL_DIR: /home/runner/.local/share
# Configure where binaries are linked (e.g. a directory that is already in PATH)
RELEASETOOLS_BINARY_DIR: /home/runner/.local/bin
# Check that releasetools was installed correctly
- run: releasetools base::check_deps
Every module is bash over git, gh and coreutils, so the action is a single
download-and-link step.
Available Commands
The releasetools CLI provides various modules and commands. Here are some common usage patterns:
Base Module
# Check version
releasetools version
# Verify all dependencies are installed
releasetools base::check_deps
# Get installation location
releasetools base::install_location
Git Module
# Create and push a release tag
releasetools git::release --major --sign --force --push v1.0.0
# Whether the working directory has uncommitted changes. A predicate: it prints
# nothing and answers with its exit status, 0 for dirty and 1 for clean.
releasetools git::is_dirty && echo "commit first"
# Print the highest release tag on HEAD, 'v' included (empty if HEAD is untagged)
releasetools git::version_tag
# Print the version tag on HEAD, or the short SHA if HEAD is untagged
releasetools git::version_or_sha
# Print the short SHA of HEAD, suffixed with '-dirty' if there are uncommitted changes
releasetools git::head_sha
# Print the most recent release version on the remote, without the 'v'
releasetools git::latest_version
# Print the remote this repository belongs to
releasetools git::remote
# Print the release tags on HEAD, highest version first
releasetools git::tags_at_head
# Refuse unless a tag on HEAD names this version
releasetools git::assert_tag_version "$(uv version --short)"
# Refuse unless the remote definitely does not carry this tag
releasetools git::assert_tag_free v1.2.3
# Refuse unless main already took HEAD
releasetools git::assert_on_branch main
git::release accepts --major/-m, --sign/-s, --push/-p and --force/-f,
plus exactly one vX.Y.Z version. Unknown flags and non-semver versions are rejected
rather than ignored. A tag that already points at HEAD is reused, so a retry after a
failed push needs no flag; one pointing anywhere else is refused.
git::remote answers from checkout.defaultRemote, then the current branch's remote,
then a sole remote, and refuses when several exist and nothing says which.
git::latest_version and git::release --push both use it rather than assuming origin.
The name says the prefix. A *_tag gives back what git tag accepts, v1.2.3, and a
*_version gives back what a manifest, a chart and a package index carry, 1.2.3. Every
command that takes one accepts either form.
The version is passed in, never read from a manifest. pyproject.toml, package.json,
Cargo.toml and Chart.yaml each need a different parser, and the CLI carries none of
them: uv version --short, node -p "require('./package.json').version" and their
equivalents already exist wherever they are needed.
The three assert_* commands refuse whenever they cannot prove what they were asked to
prove, so an unanswered question stops a release rather than passing it.
GitHub Module
Every command here shells out to the GitHub CLI, which resolves the repository from
GH_REPO when set and from the checkout's remote otherwise. GITHUB_REPOSITORY is not
part of that, so a workflow sets both:
- run: releasetools github::await_workflow "$GITHUB_SHA" tests.yml
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
# Refuse unless main already took this commit, asked over the compare API.
# Needs no local history, so it works on a shallow clone where the git:: twin cannot.
releasetools github::assert_on_branch main
# Wait for one workflow's run on a commit, and fail if that run failed
releasetools github::await_workflow "$GITHUB_SHA" tests.yml
# Refuse when a release already exists for the tag and carries assets
releasetools github::assert_release_absent v1.2.3
# Tell another repository that a release shipped
releasetools github::dispatch releasetools/homebrew-tap upstream-released version=v1.2.3
github::get_version was removed in v0.1.0. It read GITHUB_REF and printed the tag;
releasetools git::version_tag is the replacement, and --env becomes:
echo "VERSION=$(releasetools git::version_tag)" >>"$GITHUB_ENV"
Changelog Module
# Print one version's section of CHANGELOG.md, heading excluded
releasetools changelog::section 1.2.3
releasetools changelog::section 1.2.3 docs/CHANGELOG.md
A release body then carries what somebody wrote for that version rather than a list of
commit subjects. The heading match is anchored on the whole line, so asking for 1.2.3
cannot open a ## 1.2 section, or the reverse.
Net Module
# Print an HTTP status code
releasetools net::status https://pypi.org/pypi/my-package/1.2.3/json
# Wait for a URL to start serving, backing off 15s, 30s, 60s, 120s, 240s
releasetools net::await_url https://pypi.org/pypi/my-package/1.2.3/json
An index or a CDN serves what it has just accepted only after a delay, and a fixed sleep
either wastes time or fails a publish that worked. A transport failure is kept distinct
from a status code, because curl reports an outage as 000 and comparing that against
404 would read an outage as "safe to publish". Every request carries a User-Agent, which
crates.io requires.
Release Module
releasetools release::prechecks 1.2.3 \
--branch main \
--check-registry-url "https://pypi.org/pypi/my-package/1.2.3/json"
Checked in order, cheapest first: the version is x.y.z, the working tree is clean, the
tag is free on the remote, the version is after the newest release tag the remote carries,
HEAD is on the branch, and the registry answers 404 for it. PyPI, the npm registry and
crates.io all answer 404 for a version they do not carry; any other answer is a refusal
rather than a guess.
A prerelease sorts below the release it leads to, so 2.0.0 is after v2.0.0-rc1.
Listing Commands
Running releasetools with no arguments prints every available command:
releasetools
Use Cases
Release Automation
Automate your release process with standardized tagging:
name: Release
on:
push:
branches: [ main ]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: releasetools/cli@v0
- name: Create Release
run: |
# Create and push a new release tag.
# The version must be semver (vX.Y.Z) -- a date stamp such as v20260101
# is rejected, since it has no minor or patch component.
releasetools git::release --major --sign --push v1.2.3
Version Management
Manage versions across your projects:
# The version of the releasetools CLI itself
releasetools version
# The version tag on HEAD, or the short SHA if HEAD carries no tag
current_version=$(releasetools git::version_or_sha)
# Create a new release. git::release does not compute the next version for you --
# pass the full vX.Y.Z tag you intend to publish.
releasetools git::release --push v1.3.0
CI/CD Integration
Integrate with your existing CI/CD pipelines:
- name: Setup Release Tools
uses: releasetools/cli@v0
- name: Validate Environment
run: releasetools base::check_deps
- name: Deploy if Clean
run: |
if releasetools git::is_dirty; then
echo "Working directory has uncommitted changes"
exit 1
fi
echo "Working directory is clean, proceeding with deployment"
# Your deployment commands here
Development
You can find the code and development guidelines in the src/ directory.
Setting Up Development Environment
-
Clone the repository:
git clone https://github.com/releasetools/cli.git
cd cli -
Install development dependencies:
# Install any required dependencies
pip install -r requirements.txt -
Run tests:
# Run the test suite
./scripts/test.sh
Contributing
We welcome contributions! Here's how you can help:
- Fork the repository on GitHub
- Create a feature branch for your changes
- Write tests for new functionality
- Follow existing code style and conventions
- Submit a pull request with a clear description
Code Structure
src/- Main source code directoryscripts/- Build and utility scriptstests/- Test files
Releasing
Once you have completed and tested the code, see the release instructions.
Creating a New Release
# Use releasetools itself to create releases
releasetools git::release --major --sign --force --push v1.0.0
Installation Verification
After installation, verify that releasetools is working correctly:
# Check if releasetools is in PATH
which releasetools
# Verify version
releasetools version
# Check all dependencies
releasetools base::check_deps
# List available commands
releasetools
Troubleshooting
Common Issues
releasetools: command not found
- Ensure
~/.local/binis in your PATH - Verify the installation completed successfully
- Check that the symlink was created:
ls -la ~/.local/bin/releasetools - If you installed with Homebrew, the binary is in Homebrew's prefix instead, and
rtis available as a shorthand:command -v releasetools rt
Permission denied errors
- Make sure you have write permissions to the installation directories
- Consider customizing
RELEASETOOLS_INSTALL_DIRandRELEASETOOLS_BINARY_DIR
Missing dependencies
- Run
releasetools base::check_depsto identify missing requirements - Install missing dependencies based on the error messages
GitHub Action failures
- Verify the action version is correct
- Check that required environment variables are set
Getting Help
If you encounter issues:
- Check existing issues: Look at the GitHub Issues for similar problems
- Run diagnostics: Use
releasetools base::check_depsto identify environment issues - Check installation: Verify with
releasetools base::install_location - Open an issue: Provide detailed information about your environment and the problem