Elixir GitHub Actions Workflow Template
Last updated
This is a working .github/workflows/ci.yml for an Elixir or Phoenix app with Postgres. It installs Erlang/OTP and Elixir with erlef/setup-beam, caches deps and _build, checks formatting, compiles with warnings as errors and runs mix test.
The workflow
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
MIX_ENV: test
DATABASE_URL: ecto://postgres:postgres@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
id: beam
with:
otp-version: "27"
elixir-version: "1.18"
- uses: actions/cache@v4
with:
path: |
deps
_build
key: mix-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
restore-keys: |
mix-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-
- run: mix deps.get
- run: mix format --check-formatted
- run: mix compile --warnings-as-errors
- run: mix ecto.create && mix ecto.migrate
- run: mix test
What the non-obvious parts do
erlef/setup-beam@v1installs Erlang/OTP and Elixir. Pin both versions as quoted strings so YAML does not turn them into numbers. The action outputs the exact versions it installed, which the cache key uses throughid: beam.- Cache key with OTP and Elixir versions. Compiled
_buildoutput is only valid for the versions that produced it. Including them prevents loading beam files from a different version.mix.lockchanges create a fresh cache, andrestore-keysfalls back to the latest one for the same versions. depsand_buildtogether.depsholds downloaded packages,_buildholds compiled output, including dependencies. Caching both is what makes later runs fast. Compiling dependencies is the slowest part of a cold run. See speed up Elixir on GitHub Actions.MIX_ENV: testat job level makes every step use the test environment, so_build/testis the one cached and compiled.DATABASE_URLassumesconfig/runtime.exsorconfig/test.exsreads it. Phoenix generators useecto://URLs. If your test config hardcodes the database host, set it tolocalhostand the user and password above.--warnings-as-errorsfails the build on compiler warnings, keeping the codebase clean.- Service health check. Without it,
ecto.createmay run before Postgres accepts connections.
Variants
Version matrix
Add matrix.include pairs such as otp: "26", elixir: "1.17" and otp: "27", elixir: "1.18". Pair versions that are compatible. See matrix builds.
Static analysis
Add mix credo --strict and Dialyzer with dialyxir. Cache the PLT directory (priv/plts) separately, keyed on mix.lock, because building it takes many minutes.
Assets
Phoenix apps with esbuild and Tailwind need mix assets.setup and mix assets.build before tests that render pages.
Test partitions
Mix supports MIX_TEST_PARTITION with --partitions. Combine with test sharding, using a separate database per partition.
Deploy job
Add a second job on main only that builds a release with mix release, uploads it as an artifact, and deploys with OIDC.
Run it on faster machines
The BEAM runs tests concurrently, so extra cores help. Change one line:
runs-on: [self-hosted, Linux, compiler-ci]
The job runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback. In compiler.dev's benchmark a Phoenix LiveView app with Postgres went from 2m07s to 1m03s; see benchmarks. Prices are on pricing.
FAQ
Why cache _build as well as deps?
Compiling dependencies is slow, and _build keeps the compiled results.
Can I skip the OTP version in the cache key?
It is risky. Beam files compiled by one OTP version may not load under another.
How do I find compatible OTP and Elixir versions?
The Elixir compatibility table in its official documentation lists supported OTP versions for each release.
Does ecto.create hurt when the database exists?
No. It reports that the database already exists and continues.
Related
- Speed Up Elixir in GitHub ActionsSpeed up Elixir and Phoenix CI in GitHub Actions: deps and _build cache keyed on mix.lock and OTP, mix test --partitions, Postgres service, Dialyzer PLT.
- .NET GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for .NET with NuGet caching, restore, build and test steps, a SQL Server service option, concurrency and permissions.
- Django GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for Django with a Postgres service container, pip caching, migrations check and tests, plus concurrency and permissions.
- Docker Build GitHub Actions Workflow TemplateCopy-paste GitHub Actions workflow to build Docker images with Buildx, GitHub cache layers and a push to GHCR on main, with minimal permissions.