A Ruby gem that provides Rake tasks for managing code deployment workflows with automated versioning, changelog management, and Slack notifications.
Add this line to your application's Gemfile:
gem "discharger"And then execute:
$ bundle installThen run the install generator:
$ rails generate discharger:installOr install it yourself as:
$ gem install dischargerAdd require "discharger/task" to your Rakefile, then configure the discharger task:
require "discharger/task"
Discharger::Task.create do |task|
# Version management
task.version_file = "config/application.rb"
task.version_constant = "MyApp::VERSION"
# Slack integration
task.release_message_channel = "#some-slack-channel"
task.app_name = "My App Name"
# Git integration
task.commit_identifier = -> { `git rev-parse HEAD`.strip }
task.pull_request_url = "https://github.com/SOFware/some-app"
# Changelog management (optional)
task.fragment = "changelog.d" # Directory for changelog fragments
endDischarger supports changelog management through the fragment setting from Reissue.
Document changes directly in your commit messages using git trailers. Set task.fragment = :git to enable this feature:
Discharger::Task.create do |task|
# ... other configuration ...
task.fragment = :git # Enable git commit trailers
endThis keeps changelog data coupled with your code changes in the same commit. See Git Trailers Guide for detailed usage instructions.
Alternatively, you can use file-based fragments by creating individual changelog files in a directory:
Discharger::Task.create do |task|
# ... other configuration ...
task.fragment = "changelog.d" # Enable file-based fragments
endWith this approach, create individual changelog files in the changelog.d/ directory:
changelog.d/
├── 123-fix-login-bug.md
├── 124-add-user-profile.md
└── 125-update-dependencies.md
Each fragment file should contain the changelog entry for a specific change or feature.
Some releases need manual follow-up: run a rake task, backfill data, re-index documents.
Reissue collects those steps from Runbook: commit trailers, and Discharger announces
them to your team in the release thread.
Discharger::Task.create do |task|
# ... other configuration ...
task.runbook_file = "RUNBOOK.md" # Default: nil (disabled)
endAdd steps as you work, in the same commit as the change that requires them:
git commit -m "Add data migration
Fixed: Duplicate user records
Runbook: Run \`rake data:cleanup\` after deploy"rake release:prepare finalizes the runbook alongside the changelog, so the release tag
records exactly which steps that version needs. Discharger posts the checklist to Slack as
a reply in the release thread, right after the changelog:
*Post-release runbook for 1.2.3* — 2 steps
• Run `rake data:cleanup` (abc1234)
• Re-index search documents (def5678)
When a release needs no follow-up, the thread says so explicitly rather than staying silent, so nobody has to wonder whether the check was skipped:
*Post-release runbook for 1.2.3*
No runbook tasks for this release.
Leave runbook_file unset and Discharger posts nothing extra. See the
Reissue runbook documentation
for how items are collected and merged.
Discharger assumes one long-lived branch, main by default. CI deploys every push to that
branch to staging, and production deploys from v* tags. There is no staging branch and no
production branch to keep in sync.
$ rake -T release
rake release # ---------- STEP 2 ----------
rake release:config # Echo the configuration settings
rake release:prepare # ---------- STEP 1 ----------
rake release:slack[text,channel,emoji] # Send a message to Slack- Prepare (
rake release:prepare): Cut a finish branch frommain, finalize the changelog and runbook, and open a PR back tomain. Merging that PR puts the finalized release on staging. - Release (
rake release): Tag the release commit onmain, push the tag, announce the release in Slack, then open the PR that bumpsmainto the next version.
Set DISCHARGER_RELEASE_CONFIRM=1 to run release:prepare and release without a TTY.
rake release tags the newest commit that changed the current version's dated changelog
section, so work merged after finalizing (or re-finalizing for a hotfix) is left out.
Production ships the tagged commit, which can be older than what staging last ran.
Discharger::Task.create do |task|
task.app_name = "MyApp"
task.working_branch = ENV.fetch("WORKING_BRANCH", "main")
# ... other configuration
endBefore bumping the gem, CI must deploy every push to the working branch to staging and
every v* tag to production. Then:
- Remove
staging_branch,production_branch,auto_deploy_staginganddescriptionfrom the Rakefile. In 0.5.0 they warn and do nothing; 0.6 removes them. working_branchnow defaults tomain. Set it if the app releases from another branch.release:stageandrelease:buildare gone; nothing replaces them.
Discharger includes a setup script that automates your development environment configuration. When you run the install generator, it creates a bin/setup script and a config/setup.yml configuration file.
After installing Discharger, run the setup script to configure your development environment:
$ bin/setupYou can rerun it whenever your environment drifts. Every run drops and rebuilds the development and test databases, so local data is lost.
It works on a fresh clone with no gems installed. The generated script first runs a
standard-library-only pass that stores bundler credentials for a configured
github_packages source (from your GitHub CLI login, only once the source
accepts that token; gh calls time out after github_packages.gh_timeout seconds,
15 by default, or DISCHARGER_GH_TIMEOUT), installs the bundle, and then
re-execs itself under bundle exec so default gems such as psych never clash with
Gemfile.lock. The DISCHARGER_SETUP_BUNDLED environment variable marks the second
pass; the github_packages step later verifies the stored credentials and warns when
the token lacks the read:packages scope. Before pre_steps or Rails run, setup
creates every missing counterpart of config/**/*.example and .env.example, so an
app whose boot needs .env or config/database.yml gets them without copying
them in pre_steps.
Each pre_step and step prints its elapsed time when it finishes, and the closing line reports the total, so a slow setup shows which step to look at.
bin/setup is a verbatim copy of the gem's template and opens with a notice saying
so. Do not hand-edit it in an app: app-specific work belongs in config/setup.yml
(pre_steps, steps, custom_steps), and changes to the script itself belong in
this gem's template so every app picks them up. After bumping to a discharger
release that changed the template, regenerate the script without touching
config/setup.yml or the initializer:
$ bin/rails generate discharger:install --setup-only --forceTo catch drift in CI, run the check. It exits non-zero when bin/setup is missing
or differs from the installed gem's template, and names the command above:
$ bin/rails discharger:setup:checkA script generated elsewhere takes its path as the task argument:
discharger:setup:check[scripts/setup].
The setup process is configured through config/setup.yml. Here's an example configuration:
app_name: "YourAppName"
database:
port: 5432
name: "db-your-app"
version: "14"
password: "postgres"
# Optional. DB_NAME exported before Rails boots; defaults to the container
# name without "db-". false leaves it to database.yml.
# db_name: "your-app"
# Optional. Controls how the docker step handles a native PostgreSQL
# already listening on the configured port:
# omitted/nil/false - silently skip Docker and use the native instance (legacy default)
# true - always create the Docker container; fail if a native instance holds the port
# "prompt" - ask the developer (interactive shells only; non-interactive runs fall back to native)
prefer_docker: "prompt"
redis:
port: 6379
name: "redis-your-app"
version: "latest"
# Built-in commands to run
steps:
- brew
- asdf
- git
- bundler
- yarn
- config
- docker
- env
- database
# Custom commands for your application
custom_steps:
- description: "Seed application data"
command: "bin/rails db:seed"The pre_steps array defines commands that run before Rails loads. These are for system dependencies and environment setup that must be in place before bundler or Rails can initialize.
Built-in pre_steps:
homebrew- Installs Homebrew if not present (macOS)postgresql_tools- Optionally installs PostgreSQL client tools (pg_dump,psql) for apps that usestructure.sqlor call those tools directly
pre_steps:
- homebrew
# - postgresql_toolsYou can also define custom pre_steps with shell commands. config/**/*.example and
.env.example counterparts are created before pre_steps run, so a step can rely
on them, as Qualify's does here:
pre_steps:
- homebrew
- description: "Check .env against .env.example"
command: "bin/check-env"The steps array specifies which built-in setup commands to run. Available commands include:
brew- Install Homebrew dependenciesasdf- Setup version management with asdfgit- Configure git settingsgithub_packages- Check the GitHub CLI token against a private GitHub Packages gem source; the generatedbin/setupstores the credentials (requires agithub_packages.sourceconfig entry)bundler- Install Ruby gems; a check under the generatedbin/setup, whose first pass already installed the bundleyarn- Install JavaScript packagesconfig- Copy Procfile.dev to Procfile and any example config file still missingdocker- Setup Docker containerspg_tools- Create Docker-awarepg_dumpandpsqlwrappers for apps that usestructure.sqlor call those tools directlyenv- Create .env from .env.example if still missingdatabase- Drop and recreate the development and test databases, load the schema, migrate, and seed. Every run resets local data.
You can customize which steps run by modifying the steps array:
# Only run specific setup steps
steps:
- bundler
- database
- yarnLeave the array empty or omit it entirely to run all available steps, in the order listed above. Custom commands registered through Discharger::SetupRunner.register_command run last.
Add application-specific setup tasks using the custom_steps section:
custom_steps:
# Simple command
- description: "Compile assets"
command: "bin/rails assets:precompile"
# Command with condition
- description: "Setup Elasticsearch"
command: "bin/rails search:setup"
condition: "defined?(Elasticsearch)"
# Command with environment variable condition
- description: "Import production data"
command: "bin/rails db:import"
condition: "ENV['IMPORT_DATA'] == 'true'"Each custom step can include:
description- A description shown during setupcommand- The command to executecondition- An optional Ruby expression that must evaluate to true for the command to runname- An optional name that lets the step be scheduled in thestepslist
Custom steps run after the built-in steps, in the order listed. To run one
earlier, give it a name and list that name in steps:
custom_steps:
- name: import_certs
description: "Import development certificates"
command: "bin/import-certs"
steps:
- brew
- import_certs # runs between brew and bundler
- bundlerA named step listed in steps runs only at that position; named steps that
are not listed keep the default run-last behavior. A name that collides with
a built-in step resolves to the built-in, and a warning is logged.
For more complex setup logic, you can create custom command classes that integrate with Discharger's setup system:
# lib/setup_commands/elasticsearch_command.rb
class ElasticsearchCommand < Discharger::SetupRunner::Commands::BaseCommand
def description
"Configure Elasticsearch"
end
def can_execute?
defined?(Elasticsearch)
end
def execute
with_spinner("Setting up Elasticsearch...") do
# Your setup logic here
system("bin/rails search:setup")
system("bin/rails search:reindex")
end
log "Elasticsearch configured successfully", emoji: "✅"
end
end
# Register the command in an initializer or your setup script
Discharger::SetupRunner.register_command(:elasticsearch, ElasticsearchCommand)Then use it in your configuration:
steps:
- bundler
- database
- elasticsearch # Your custom commandThis gem is managed with Reissue.
Releases are streamlined with a single GitHub Actions workflow using RubyGems Trusted Publishing:
- Go to Actions → "Release gem to RubyGems.org" → Run workflow
- Select version bump type (patch, minor, or major)
- The workflow will automatically:
- Finalize the changelog with the release date
- Build the gem with checksum verification
- Publish to RubyGems.org via Trusted Publishing (no API keys needed)
- Create a git tag for the release
- Bump to the next development version
- Open a PR with the version bump for continued development
Bug reports and pull requests are welcome on GitHub.
The gem is available as open source under the terms of the MIT License.