Automating Elixir Package Deployment with GitHub Actions
Tags: elixir, github-actions • Categories: Learning
I enjoy Elixir, although I know it doesn’t make sense to use it for most production applications. However, I try to use it for side projects when I can, even just because it’s such a fun and beautiful language to program in.
One thing I’ve been fascinated with recently is automating the entire release process for packages using conventional commits with GitHub actions. This post walks through how to make that happen, and some other bits around Elixir project setup. Here’s the example project with all of the learnings from this post in place.
mix.exs project configuration
Add some key deps to mix.exs
you’ll need for build & deploy:
def project do
[
test_coverage: [tool: ExCoveralls],
deps: deps()
]
end
defp deps do
[
{:excoveralls, "~> 0.17.1", only: :test},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
]
end
Make sure you have extra fields in your mix.exs
to avoid the Missing metadata fields: description, licenses, links
error.
Note that unlike many other languages keywords
is not a valid option in package()
.
def project do
[
...
package: package(),
...
]
end
defp package do
[
description:
"An Elixir utility to execute SQL files on Postgres using psql CLI, supporting multi-statement SQL with mock testing capabilities",
files: ["lib", "mix.exs", "README.md", "CHANGELOG.md"],
maintainers: ["Michael Bianco"],
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/iloveitlay/postgres_executor"}
]
end
Automated test, build, and release
I wanted the following features using GitHub actions:
- Code formatting
- Unused dependencies
- Test coverage
- Test
- Postgres
- Changelog generation
- Build & publish to mix
- GitHub release generation
To support this, I had to hack in mix support into the changelog action.
# base configuration pulled from:
# https://github.com/dashbitco/broadway/blob/master/.github/workflows/ci.yml
name: Build & Publish
on:
pull_request:
push:
branches:
- "*"
# by default, permissions are read-only, read + write is required for git pushes
permissions:
contents: write
env:
MIX_ENV: test
jobs:
test:
runs-on: "ubuntu-latest"
services:
postgres:
# replace with postgis/postgis:
, use dashes instead of dots for version
image: postgres:15.3
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
name: Test Elixir ${{ matrix.elixir }}, OTP ${{ matrix.otp }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
elixir: ["1.15", "1.14", "1.13"]
otp: ["26", "25"]
exclude:
- elixir: "1.13"
otp: "26"
include:
- elixir: "1.15"
otp: "26"
coverage: coverage
lint: lint
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
- name: Restore Dependency Cache
uses: actions/cache@v3
id: cache-deps
with:
path: |
deps
_build
key: |
mix-${{ runner.os }}-${{matrix.elixir}}-${{matrix.otp}}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
mix-${{ runner.os }}-${{matrix.elixir}}-${{matrix.otp}}-
- run: mix deps.get
if: steps.cache-deps.outputs.cache-hit != 'true'
- run: mix deps.compile --warnings-as-errors
if: steps.cache-deps.outputs.cache-hit != 'true'
- name: Check for unused deps
run: mix deps.unlock --check-unused
if: ${{matrix.lint}}
- run: mix format --check-formatted
if: ${{matrix.lint}}
- name: Check for abandonded packaged
run: mix hex.audit
if: ${{matrix.lint}}
- name: Set up database
run: |
PGPASSWORD=postgres psql -c 'create database postgres_test;' -U postgres -h localhost -p 5432
- run: mix test
- run: mix coveralls.github
if: ${{matrix.lint}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Changelog
if: ${{matrix.lint}}
id: changelog
# change to uses: TriPSs/conventional-changelog-action@v4
once PR is merged
uses: iloveitaly/conventional-changelog-action@elixir-support
with:
github-token: ${{ secrets.github_token }}
fallback-version: "0.1.0"
version-file: "./mix.exs"
output-file: "CHANGELOG.md"
- name: Build & Publish
run: |
mix hex.build
mix hex.publish --yes
if: ${{matrix.lint && steps.changelog.outputs.skipped == 'false'}}
env:
MIX_ENV: dev
# gh secret set HEX_API_KEY --app actions --body $HEX_API_KEY
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
- name: Github Release
if: ${{ matrix.lint && steps.changelog.outputs.skipped == 'false' }}
uses: softprops/action-gh-release@v1
with:
# output options: https://github.com/TriPSs/conventional-changelog-action#outputs
body: ${{ steps.changelog.outputs.clean_changelog }}
tag_name: ${{ steps.changelog.outputs.tag }}
Dependabot
For automated dependency updates add this to .github/dependabot.yml
:
version: 2
updates:
- package-ecosystem: mix
directory: "/"
schedule:
interval: monthly
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
Quickly using these actions with new projects
I hack on a bunch of open source projects. It’s a pain to have to remember the folder structure for the .github/*
related bits.
One thing that makes this slightly easier for me is shell history combined with a cli alias touchp
which will create a file and it’s folder path in one command. This makes it easy to type touchp .g
and cycle through past yml file paths to easily generate the folder structure to copy/paste the above code into.
Readme badges
Here’s some nice badges that you can quickly use by subbing the USERNAME and REPO with your own.
[![Module Version](https://img.shields.io/hexpm/v/REPO.svg)](https://hex.pm/packages/REPO)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/REPO/)
[![Total Download](https://img.shields.io/hexpm/dt/REPO.svg)](https://hex.pm/packages/REPO)
[![License](https://img.shields.io/hexpm/l/REPO.svg)](https://github.com/USERNAME/REPO/blob/master/LICENSE.md)
![github actions badge](https://github.com/USERNAME/REPO/actions/workflows/build_and_publish.yml/badge.svg)