# EasyPublish
A complete release tool for Hex packages. Updates version numbers, runs pre-release checks, updates changelog, commits, tags, pushes, creates GitHub release, and publishes to Hex.
## Installation
Add `easy_publish` to your list of dependencies in `mix.exs` as a dev-only dependency:
```elixir
def deps do
[
{:easy_publish, "~> 0.2", only: :dev, runtime: false}
]
end
```
## Usage
Perform a full release:
```bash
# Bump patch version (0.1.0 -> 0.1.1)
mix easy_publish.release patch
# Bump minor version (0.1.0 -> 0.2.0)
mix easy_publish.release minor
# Bump major version (0.1.0 -> 1.0.0)
mix easy_publish.release major
# Release current version as-is (for initial release)
mix easy_publish.release current
# Set explicit version
mix easy_publish.release 2.0.0
```
Run checks only (no changes made):
```bash
mix easy_publish.release patch --dry-run
```
## Release Flow
EasyPublish uses a step-based architecture with two pipelines:
1. **Check pipeline** - Validates preconditions before any changes are made
2. **Release pipeline** - Performs the actual release (version bump, commit, tag, publish)
Each step implements an `execute/1` callback. If any step fails, the pipeline halts.
### Default Steps
| # | Step | Module | Phase | Description |
|---|------|--------|-------|-------------|
| 1 | Git working directory is clean | `GitClean` | check | Ensures no uncommitted changes exist |
| 2 | On correct branch | `GitBranch` | check | Verifies current branch matches expected (default: `main`) |
| 3 | Git is up to date with remote | `GitUpToDate` | check | Checks local branch isn't behind/ahead/diverged from remote |
| 4 | Tests pass | `Tests` | check | Runs `mix test` |
| 5 | Code is formatted | `Format` | check | Runs `mix format --check-formatted` |
| 6 | Credo analysis passes | `Credo` | check | Runs `mix credo --strict` (skipped if not installed) |
| 7 | Dialyzer passes | `Dialyzer` | check | Runs `mix dialyzer` (skipped if not installed) |
| 8 | Changelog | `Changelog` | check+run | Validates UNRELEASED section exists, then updates it |
| 9 | Hex package builds successfully | `HexBuild` | check | Runs `mix hex.build` to validate package |
| 10 | Updating version files | `UpdateVersion` | run | Updates `@version` in mix.exs and README.md dependency |
| 11 | Committing release | `GitCommit` | run | Commits mix.exs, README.md, and CHANGELOG.md |
| 12 | Creating git tag | `GitTag` | run | Creates annotated tag `vX.Y.Z` |
| 13 | Pushing to remote | `GitPush` | run | Pushes commit and tag to remote |
| 14 | Creating GitHub release | `GitHubRelease` | run | Creates GitHub release via `gh` CLI (skipped if not available) |
| 15 | Publishing to Hex | `HexPublish` | run | Runs `mix hex.publish --yes` |
All step modules are under `EasyPublish.Steps.*`.
### Custom Steps
Create custom steps by implementing the `EasyPublish.Step` behaviour:
```elixir
defmodule MyApp.Steps.NotifySlack do
use EasyPublish.Step, name: "Notify Slack"
# `use EasyPublish.Step` imports helper functions: info/1, warn/1, error/1,
# git/1, run_mix_task/1, has_dep?/1, has_executable?/1
@impl true
def options do
[{:slack_webhook, type: :string, required: true, doc: "Slack webhook URL"}]
end
@impl true
def execute(ctx) do
if ctx.dry_run do
info("Would notify Slack")
:ok
else
# Send notification
:ok
end
end
end
```
**Return values:**
- `:ok` - Success
- `{:ok, updated_ctx}` - Success with updated context
- `:skip` - Skipped (no reason shown)
- `{:skip, reason}` - Skipped with reason
- `{:error, reason}` - Failure, halts pipeline
### Configuring Steps
```elixir
# config/config.exs
# Replace all default steps entirely
config :easy_publish,
check_steps: [
EasyPublish.Steps.GitClean,
EasyPublish.Steps.Tests
],
release_steps: [
EasyPublish.Steps.UpdateVersion,
MyApp.Steps.CustomStep,
EasyPublish.Steps.HexPublish
]
# Or modify defaults
config :easy_publish,
prepend_check_steps: [MyApp.Steps.BeforeChecks],
append_release_steps: [MyApp.Steps.NotifySlack],
skip_steps: [EasyPublish.Steps.Dialyzer]
```
## Changelog Format
Your `CHANGELOG.md` should have an UNRELEASED section:
```markdown
# Changelog
## UNRELEASED
- Added new feature
- Fixed bug
## 0.1.0 - 2024-01-15
- Initial release
```
When you run `mix easy_publish.release minor` for version 0.2.0, it becomes:
```markdown
# Changelog
## 0.2.0 - 2024-01-20
- Added new feature
- Fixed bug
## 0.1.0 - 2024-01-15
- Initial release
```
## Options
| Flag | Description |
|------|-------------|
| `--dry-run` | Only run checks, don't make any changes |
| `--skip-tests` | Skip running tests |
| `--skip-format` | Skip format check |
| `--skip-credo` | Skip credo analysis |
| `--skip-dialyzer` | Skip dialyzer |
| `--skip-changelog` | Skip changelog check |
| `--skip-git` | Skip all git checks |
| `--skip-hex-build` | Skip hex.build validation |
| `--skip-github-release` | Skip GitHub release creation |
| `--branch NAME` | Required branch name (default: "main") |
| `--changelog-entry CONTENT` | Add a changelog entry and skip UNRELEASED check |
### Quick releases with `--changelog-entry`
For quick releases where you don't want to manually edit the changelog first:
```bash
# Add a changelog entry and release in one command
mix easy_publish.release patch --changelog-entry "Fixed authentication bug"
# Multiple changes can be separated by newlines
mix easy_publish.release minor --changelog-entry "Added user profiles
Fixed memory leak
Updated dependencies"
```
This will:
1. Add the entry to the UNRELEASED section (creating it if needed)
2. Skip the UNRELEASED section check
3. Proceed with the normal release flow
## Configuration
Configure defaults in your `config/config.exs`:
```elixir
config :easy_publish,
branch: "main",
changelog_file: "CHANGELOG.md",
skip_github_release: false,
skip_tests: false,
skip_format: false,
skip_credo: false,
skip_dialyzer: false,
skip_changelog: false,
skip_git: false,
skip_hex_build: false
```
CLI flags always override configuration.
### Example configurations
**CI-friendly config** (skip slow checks locally, run them in CI):
```elixir
# config/dev.exs
config :easy_publish,
skip_dialyzer: true,
skip_credo: true
```
**Non-GitHub project** (skip GitHub release creation):
```elixir
config :easy_publish,
skip_github_release: true
```
**Custom branch workflow**:
```elixir
config :easy_publish,
branch: "develop"
```
### CLI examples
```bash
# Full release with all checks
mix easy_publish.release patch
# Quick release skipping slow checks
mix easy_publish.release patch --skip-dialyzer --skip-credo
# Dry run to validate everything first
mix easy_publish.release minor --dry-run
# Release from a feature branch (not recommended for production)
mix easy_publish.release patch --branch feature/my-branch --skip-git
# Initial release of a new package
mix easy_publish.release current
# Quick bugfix release
mix easy_publish.release patch --changelog-entry "Fixed crash on startup"
```
## GitHub Release
GitHub releases are created automatically using the `gh` CLI if:
- `gh` CLI is installed
- The repository is hosted on GitHub
- `--skip-github-release` is not set
If `gh` is not installed or the repo is not on GitHub, this step is silently skipped.
To install `gh`:
- macOS: `brew install gh`
- Linux: See https://github.com/cli/cli#installation
## License
MIT