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.

8 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!