Erlang-ci - Standardized GitHub Actions CI for Erlang/OTP projects

I got tired of copying and tweaking the same 50–100 lines of CI YAML across every Erlang project, so I built erlang-ci — a reusable GitHub Actions workflow and composite action for Erlang/OTP projects.

The before/after is pretty simple:

yaml

# before: 50-120 lines of setup-beam, caching, compile, fmt, xref, dialyzer, eunit...
# repeated in every repo, slowly drifting apart

# after:
uses: Taure/erlang-ci/.github/workflows/ci.yml@v1
with:
  otp-version: '28'

After compile, all enabled checks run in parallel — fmt, xref, dialyzer, eunit, CT, ex_doc, and audit. Most are on by default, optional ones (CT, coverage, ex_doc, audit) are opt-in.

There’s also built-in support for OTP version matrix testing, PostgreSQL service containers for database-backed tests, and auto-detection of .tool-versions/mise.toml.

It’s already in use across the Nova ecosystem — Nova, Kura, and a few rebar3 plugins. The configs there show real-world examples ranging from simple libraries to full apps with Postgres and CT.

Two usage modes:

  • Reusable workflow — drop-in complete pipeline

  • Composite action — just the setup + caching, bring your own jobs

Repo:

Feedback and PRs welcome — especially curious if there are common CI patterns in the community I’ve missed.

10 Likes

Since the initial post, quite a bit has been added — here’s a rundown of what’s new.

SBOM generation (enable-sbom)

Opt-in SBOM generation via rebar3 sbom, producing a CycloneDX-format Software Bill of Materials for your dependency tree. Useful for supply chain transparency and increasingly relevant given the EU Cyber Resilience Act requirements coming down the line.

SBOM vulnerability scan (enable-sbom-scan)

Builds on the SBOM step by running the generated CycloneDX file through Grype. The build fails on high or critical severity findings. Duplicate matches (same CVE appearing from multiple data sources) are deduplicated automatically.

Dependency submission (enable-dependency-submission)

Submits your resolved dependency graph to GitHub’s Dependency Graph API — no extra plugins needed, it’s self-contained. This gives you Dependabot alerts and lets security teams see your full transitive dependency tree directly in the GitHub Security tab.

PR summary comments (enable-summary, on by default)

When any security scanning is active (enable-audit or enable-sbom-scan), a single unified comment is posted on the PR with all scan results formatted as a table — severity, package, version, advisory, and fix version. On re-runs the comment is updated in-place, never duplicated. Requires pull-requests: write on the caller job.

Audit severity threshold (audit-level)

The rebar3_audit step now has a configurable audit-level input (low / medium / high / critical). Default is low — all findings fail the build — but you can set it to high or critical if you want lower-severity issues to still be reported in the PR comment without failing the build.

Kafka service containers (kafka: true)

Same pattern as the PostgreSQL support — spins up a Kafka broker for eunit and CT jobs. Available at localhost:9092, with KAFKA_HOST and KAFKA_PORT injected as environment variables.

Automated releases (release workflow)

A new reusable release workflow using git-cliff that auto-tags and creates GitHub releases from conventional commits. It analyzes commits since the last tag, determines the next semver bump (feat: → minor, fix: → patch, breaking → major), creates the tag, and generates a changelog. Skips silently if there’s nothing to bump. Chain it after CI with needs: ci and it only runs on push to main.

needs: chaining and company wrapper pattern

The README now has a dedicated section on extending the pipeline with your own jobs — including a full ci → black-box → deploy-staging → release example and a pattern for wrapping erlang-ci in a company-internal reusable workflow to enforce org-wide policies across all repos.


These are all flows I use across my own projects (Nova, Kura, the rebar3 plugins), so they reflect real patterns rather than hypotheticals. Happy to get feedback on what’s missing or what could work differently — especially curious if there are common CI setups in the community I haven’t covered yet!

1 Like

@Taure Nice. Did you consider adding elp lint and elp eqwalize-all to the list of checks?

hmm.. no. (in the way I did only use rebar3 plugins) :slight_smile: But maybe it can be done in a good way? do you have anything on github actions to start it or I can look into?

It should work now

1 Like

Nice, thanks for adding it!

Feel free to try it on the Nova framework or other projects, and let me know if something does not work as expected. I can help setting it up.

Btw, the Kura link returns a 404. Maybe it’s a private repo?

Hmm.. it should be public: GitHub - Taure/kura: Database layer for Erlang · GitHub I am testing both elp lint and so here.

Ah, the link is wrong:

https://github.com/novaframework/kura

I saw the usage. One thing we can do in ELP is to introduce --min-severity flag, so that only issues with that severity and above are reported. Feel free to open an issue for it, or even make a contribution.

I also see that most of the issues are related to:

Instead of passing CI despite those issues, you have two possibilities:

  1. Fix them with one line:

elp lint –diagnostic-filter binary_string_to_sigil –apply-fix –one-shot

  1. If you cannot (e.g. you support older OTP versions), or you prefer the old syntax, disable them. Add a .elp_Lint.toml to the project and add:

[linters.binary_string_to_sigil]
enabled = false

That could be nice.

v2.0.0 - Composable actions & immutable tags

Two big changes in this release:

Standalone composite actions

The monolithic workflow has been split into independent composite actions. You can now use individual tools without the full pipeline:

steps:

  • uses: actions/checkout@v4
  • uses: Taure/erlang-ci@v2.0.0
    with:
    otp-version: ‘28’
  • uses: Taure/erlang-ci/dialyzer@v2.0.0
    with:
    otp-version: ‘28’

Available actions:

  • Taure/erlang-ci — Setup: OTP, rebar3, caching
  • Taure/erlang-ci/dialyzer — PLT caching + rebar3 dialyzer
  • Taure/erlang-ci/elp — ELP install + lint and/or eqWAlize
  • Taure/erlang-ci/audit — rebar3 audit with structured JSON output
  • Taure/erlang-ci/sbom — CycloneDX SBOM + Trivy vulnerability scan
  • Taure/erlang-ci/mutate — Mutation testing with rebar3 mutate

The full reusable workflow still exists for those who want the complete pipeline with one line. It now uses these actions internally.

This follows the same pattern as gradle/actions and github/codeql-action — one repo, subdirectory per action.

Immutable release tags

After the trivy-action supply chain incident where a compromised maintainer account force-pushed a malicious tag, we’ve hardened our release process:

  • All v* tags are protected by a repository ruleset — no deletion, no force-push, no bypass actors
  • Floating major tags (@v1) are gone — pin to exact semver (e.g. @v2.0.0)
  • All third-party action references are SHA-pinned
  • Workflows are scanned by zizmor and actionlint on every PR

Migration from v1

Replace @v1 with @v2.0.0. The reusable workflow inputs are unchanged.

before

uses: Taure/erlang-ci/.github/workflows/ci.yml@v1

after

uses: Taure/erlang-ci/.github/workflows/ci.yml@v2.0.0

1 Like