Crafting the Definitive Erlang CI Pipeline with GitHub Actions: A Comprehensive Guide
Hi, I’m Matheus de Camargo Marques, a passionate enthusiast of the Erlang ecosystem. I currently work with Elixir, focusing on edge…

Timeless collaboration in the pursuit of knowledge.
Crafting the Definitive Erlang CI Pipeline with GitHub Actions: A Comprehensive Guide
Hi, I’m Matheus de Camargo Marques, a passionate enthusiast of the Erlang ecosystem. I currently work with Elixir, focusing on edge technologies and emerging innovations. I’m driven by the challenge of building scalable, resilient systems using the power of the BEAM virtual machine.
Keywords: Continuous Integration, Erlang/OTP, GitHub Actions, Automated Software Testing, Static Analysis, Code Coverage, Build Automation, Rebar3.
Abstract
The Erlang/OTP ecosystem provides a powerful platform for building robust, concurrent, and distributed systems. However, ensuring software quality, maintainability, and forward-compatibility across its evolving versions presents a significant engineering challenge. This paper presents a comprehensive and reproducible methodology for implementing a Continuous Integration (CI) pipeline for Erlang projects. We detail a robust workflow designed using GitHub Actions and the Rebar3 build tool, which systematically addresses compatibility, code quality, and testing. The proposed methodology leverages a matrix build strategy to validate the codebase against multiple, specific combinations of Erlang/OTP and Rebar3 versions. Furthermore, it integrates critical quality assurance steps, including dependency caching for performance optimization, static code analysis with Dialyzer, automated execution of unit (EUnit) and integration (Common Test) suites, and the generation of code coverage reports via Codecov. The resulting CI pipeline provides a complete, automated framework that enhances developer productivity and significantly improves the reliability and stability of Erlang applications. This work serves as a practical blueprint for development teams seeking to adopt modern DevOps practices within the Erlang ecosystem.
Introduction: Why Your Erlang Project Deserves a World-Class CI
In the landscape of modern software development, Continuous Integration (CI) and Continuous Delivery/Deployment (CD) have transitioned from being aspirational practices to fundamental necessities. For a language like Erlang, renowned for its concurrency, distribution, and fault-tolerance capabilities, a robust CI pipeline is not just beneficial — it’s crucial. A well-implemented CI system acts as the first line of defense, catching bugs early, ensuring consistent build artifacts, and fostering better collaboration within development teams. It allows developers to merge changes with confidence, knowing that a suite of automated checks will validate their contributions.
This article introduces an Erlang CI workflow for GitHub Actions designed to be comprehensive and highly effective. It’s a “complete” solution because it integrates several advanced features:
- Testing against multiple Erlang/OTP and Rebar3 versions simultaneously using matrix builds.
- Intelligent caching of dependencies and build artifacts to significantly speed up pipeline execution.
- A thorough testing regimen including static analysis with Dialyzer, unit tests with EUnit, and integration tests with Common Test.
- Automated code coverage reporting via Codecov to provide insights into test effectiveness.
- Organized management of build artifacts, such as logs and HTML coverage reports, for easy access and debugging.
The goal of this guide is to dissect this battle-tested GitHub Actions workflow, explain its inner workings in detail, and empower Erlang developers to implement a similarly powerful CI pipeline for their own projects. By the end, readers will understand:
- How to structure a sophisticated GitHub Actions workflow tailored for Erlang.
- Techniques for robustly testing their code against a variety of Erlang/OTP and Rebar3 versions.
- Best practices for implementing caching strategies that accelerate build times without compromising correctness.
- How to integrate static analysis tools and comprehensive test suites into their CI process.
- The steps to set up automated code coverage reporting to monitor and improve test quality.
- Effective methods for managing and utilizing build outputs for diagnostics and record-keeping.
Laying the Foundation: Workflow Basics
Every GitHub Actions workflow begins with a YAML file placed in a specific directory within the repository. This file defines the events that trigger the workflow, the jobs to be run, and the steps within those jobs.
File Location and Naming
GitHub Actions workflows are defined in YAML files located in the .github/workflows/ directory of a repository. For this guide, the example workflow is named
ubunto_workflow_matrix.yml. While the name can be anything descriptive, a clear name helps in managing multiple workflows if a project has several.
Workflow Name (name)
The name key at the top level of the YAML file provides a human-readable name for the workflow.
name: Erlang CI on Ubuntu
This name, “Erlang CI on Ubuntu,” will be displayed in the GitHub Actions UI, making it easy to identify this specific workflow among others in the repository’s “Actions” tab.
Workflow Triggers (on)
The on key specifies the events that will trigger the workflow.
on: [push, pull_request]
This configuration means the workflow will execute automatically on every push event to any branch in the repository and on every pull_request event that targets any branch. This ensures that all code changes, whether pushed directly or proposed via a pull request, are validated by the CI pipeline.
While these broad triggers provide maximum coverage, for very large or active repositories, developers might eventually consider more specific triggers to optimize CI resource consumption. For example, one could restrict triggers to specific branches (e.g.,
main, develop) or to changes affecting specific file paths. However, for most Erlang libraries or standalone applications, the [push, pull_request] configuration provides a comprehensive and desirable level of automated testing.
Workflow Permissions (permissions)
The permissions block configures the default permissions granted to the GITHUB_TOKEN for the entire workflow.
permissions:
contents: read # Necessário para actions/checkout
This workflow explicitly sets contents: read. This adheres to the principle of least privilege, a crucial security best practice in CI/CD environments. By granting only
read access to the repository's contents, the workflow limits the potential impact if the GITHUB_TOKEN were ever compromised during a run. The actions/checkout action, which is used to clone the repository code into the runner environment, requires this read permission. Many workflows might operate with default, broader permissions; explicitly restricting them, as done here, demonstrates a mature approach to CI security. A compromised workflow with write access could, in a worst-case scenario, push malicious code or alter repository settings. Restricting permissions to read significantly minimizes this risk.
The Power of Parallelism: Mastering Multi-Version Testing with Matrix Strategy
One of the most powerful features of GitHub Actions is the “matrix strategy,” which allows a job to be run multiple times with different configurations. This is ideal for testing Erlang projects across various OTP and Rebar3 versions.
Job Definition (jobs: build)
Workflows are composed of one or more jobs. This workflow defines a single job named build:
jobs:
build:
#... job configuration...
Each job runs in a fresh environment on a specified runner.
Custom Job Name (name: OTP ${{ matrix.otp }} Rebar3 ${{ matrix.rebar3 }})
Within the build job, a dynamic name is specified:
name: OTP ${{ matrix.otp }} Rebar3 ${{ matrix.rebar3 }}
This uses context variables from the matrix strategy (matrix.otp and matrix.rebar3) to assign a unique, descriptive name to each job run in the GitHub UI. For example, a run might be labeled "OTP 27.3.4 Rebar3 3.24.0". This immediate clarity is invaluable when inspecting workflow runs, especially when a specific combination fails.
Runner Specification (runs-on: ubuntu-24.04)
The runs-on key defines the type of machine to run the job on.
runs-on: ubuntu-24.04
This job will execute on a GitHub-hosted runner using the ubuntu-24.04 image. The erlef/setup-beam action, used later, supports various Ubuntu versions, and ubuntu-24.04 represents a modern and stable choice. Using a specific runner version like
ubuntu-24.04 rather than ubuntu-latest is a recommended practice for ensuring build stability and reproducibility. It prevents unexpected failures that might arise from unannounced breaking changes in thelatest runner image.
The Matrix Strategy (strategy)
The strategy key configures the matrix build.
strategy:
fail-fast: false
matrix:
include:
#... OTP and Rebar3 combinations...
**fail-fast: false: This is a critical setting for comprehensive multi-version testing. By default (fail-fast: true), GitHub Actions** will cancel all currently running jobs in a matrix if any single job fails. Settingfail-fast: falseensures that all jobs in the matrix will run to completion, regardless of whether other jobs fail. This is essential for getting a complete overview of compatibility across all specified OTP and Rebar3 versions. For instance, if a test fails on an older OTP version, development teams still need to know if it passes on newer versions. This setting guarantees that all results are collected, which is a hallmark of a thorough testing strategy.- Defining Specific Combinations (
matrix: include): Theincludekey undermatrixallows for the definition of an explicit list of configurations to run. This is more precise than defining separate axes forotpandrebar3(which would create a Cartesian product of all combinations) and thenexcludeing incompatible ones. For Erlang, where specific Rebar3 versions are compatible with certain ranges of OTP versions, theincludeapproach allows developers to list only known compatible or specifically targeted pairs. This reduces CI run times by avoiding known-bad combinations and makes the workflow configuration cleaner and more maintainable.
The include block in the provided workflow specifies several OTP and Rebar3 version pairs:
include:
# rebar3 3.24.0 é compatível com OTP 25-27
- otp: '25.3.2.21'
rebar3: '3.24.0'
- otp: '26.2.5.12'
rebar3: '3.24.0'
- otp: '27.3.4'
rebar3: '3.24.0'
# rebar3 3.25.0 é compatível com OTP 26-28
- otp: '26.2.5.12'
rebar3: '3.25.0'
- otp: '27.3.4'
rebar3: '3.25.0'
- otp: '28.0'
rebar3: '3.25.0'
# Adicionadas versões compatíveis para OTP 24.3.4.17
# rebar3 3.21.0, 3.20.0 e 3.19.0 são compatíveis com OTP 24
- otp: '24.3.4.17'
rebar3: '3.21.0'
- otp: '24.3.4.17'
rebar3: '3.20.0'
- otp: '24.3.4.17'
rebar3: '3.19.0'
This structure clearly defines the testing surface for the project.
Table: Supported OTP/Rebar3 Matrix Combinations
The following table summarizes the Erlang/OTP and Rebar3 combinations tested by this CI workflow, derived from the matrix.include configuration:
+-------------+----------------+--------------------------------------+
| OTP Version | Rebar3 Version | Notes/Compatibility |
+-------------+----------------+--------------------------------------+
| 25.3.2.21 | 3.24.0 | Rebar3 3.24.0 supports OTP 25-27 |
| 26.2.5.12 | 3.24.0 | Rebar3 3.24.0 supports OTP 25-27 |
| 27.3.2004 | 3.24.0 | Rebar3 3.24.0 supports OTP 25-27 |
| 26.2.5.12 | 3.25.0 | Rebar3 3.25.0 supports OTP 26-28 |
| 27.3.2004 | 3.25.0 | Rebar3 3.25.0 supports OTP 26-28 |
| 28.0 | 3.25.0 | Rebar3 3.25.0 supports OTP 26-28 |
| 24.3.4.17 | 3.21.0 | Rebar3 3.21.0 compatible with OTP 24 |
| 24.3.4.17 | 3.20.0 | Rebar3 3.20.0 compatible with OTP 24 |
| 24.3.4.17 | 3.19.0 | Rebar3 3.19.0 compatible with OTP 24 |
+-------------+----------------+--------------------------------------+
This table provides a clear, at-a-glance view of the environments against which the code is validated, making it easier for developers to understand the breadth of testing and adapt it for their own project’s supported versions.
Setting Up the Erlang Environment: erlef/setup-beam
With the job structure and matrix strategy defined, the next phase involves setting up the specific Erlang/OTP and Rebar3 versions for each job run.
Step 1: Checkout Code
steps:
- name: Checkout code
uses: actions/checkout@v4
This is a standard first step in most GitHub Actions workflows. It uses the official actions/checkout action (pinned to version v4 for stability) to download the repository's source code into the runner's workspace, making it available for subsequent steps like compilation and testing. Pinning actions to a specific version or commit SHA is a best practice to ensure workflow stability and security.
Step 2: Set up BEAM
- name: Set up BEAM
uses: erlef/setup-beam@v1
id: setup-beam
with:
otp-version: ${{ matrix.otp }}
rebar3-version: ${{ matrix.rebar3 }}
This step utilizes the erlef/setup-beam action, which is the community-standard and recommended way to set up Erlang/OTP, Elixir, Gleam, Rebar3, and Hex within GitHub Actions workflows. Before such actions, developers had to manually script these installations, which was often complex and error-prone.
erlef/setup-beam standardizes and simplifies this process significantly.
**id: setup-beam**: Assigning anidto this step allows its outputs to be referenced in subsequent steps. For example, the resolved OTP version can be accessed viasteps.setup-beam.outputs.otp-version.- Inputs (
with):otp-version: ${{ matrix.otp }}: This dynamically provides the Erlang/OTP version for the current job run, taken directly from the matrix configuration.erlef/setup-beamhandles the installation of this specific OTP version. It's generally recommended to use exact versions for OTP for the most predictable results. rebar3-version: ${{ matrix.rebar3 }}: Similarly, this sets the Rebar3 version for the current job run based on the matrix.- Outputs: The
erlef/setup-beamaction provides several useful outputs, including the exactotp-versionandrebar3-versionthat were set up. These outputs are particularly useful for constructing precise cache keys and naming artifacts, as will be seen in later steps.
The erlef/setup-beam action is a cornerstone of this flexible CI setup, as it makes switching between different Erlang and Rebar3 versions across matrix jobs trivial and reliable.
Blazing Fast Builds: Strategic Caching with actions/cache
Repeatedly downloading dependencies or rebuilding artifacts like Dialyzer’s Persistent Lookup Tables (PLTs) on every CI run can be time-consuming and inefficient. GitHub Actions provides the actions/cache@v4 action to store and restore such files, significantly speeding up workflow executions. Examples of caching for various languages and tools can be found, and the principles apply well to Erlang.
Caching Hex Packages
- name: Restore Hex package cache
uses: actions/cache@v4
id: hex-cache
with:
path: |
~/.cache/rebar3/hex
~/.hex
key: ${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock') }}
restore-keys: |
${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}-
This step configures caching for Hex packages, which are Erlang’s package manager dependencies.
**id: hex-cache**: This ID allows later steps to check if the cache was successfully restored (a "cache hit").
Paths (path):
~/.cache/rebar3/hex~/.hexThese are standard directory locations where Rebar3 and Hex store downloaded package tarballs and metadata. Caching these paths means that if dependencies haven't changed, they don't need to be re-downloaded from Hex.pm.
Cache Key (key): ${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock') }} The key is crucial for determining cache uniqueness and validity. It's composed of:
${{ runner.os }}: Ensures the cache is OS-specific (e.g., Linux caches are separate from macOS or Windows caches).${{ steps.setup-beam.outputs.otp-version }}: Makes the cache specific to the Erlang/OTP version being used. Dependencies can differ or require different compilations based on the OTP version. Using the output fromsetup-beamensures the exact resolved OTP version is part of the key.${{ hashFiles('**/rebar.lock') }}: This is the most dynamic part.rebar.lockis a file that Rebar3 generates, listing the exact versions of all resolved dependencies. If this file changes (e.g., a dependency is updated, added, or removed), its hash will change, thus invalidating the current cache and forcing a fresh fetch of dependencies. This is the most reliable way to ensure the cache reflects the true dependency state.
Restore Keys (restore-keys): ${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}- If an exact match for the key is not found, GitHub Actions will attempt to find a cache that matches one of the restore-keys (which act as prefixes). In this case, it will look for the most recent cache for the same OS and OTP version, even if rebar.lock has changed. This can still provide a significant speedup, as many dependencies might remain the same even if one or two have changed. This balances cache specificity (for correctness) with a higher hit rate (for speed).
Caching Dialyzer PLT
- name: Restore Dialyzer PLT cache
uses: actions/cache@v4
id: plt-cache
with:
path: |
~/.cache/rebar3/plt_*.ets
_build/default/rebar3_*.plt
key: ${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock', '**/rebar.config') }}
restore-keys: |
${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}-
Dialyzer, Erlang’s static analysis tool, builds a Persistent Lookup Table (PLT) containing type information about the project’s code and its dependencies. Generating this PLT can be time-consuming, especially for large projects, making it an excellent candidate for caching.
Paths (path):
~/.cache/rebar3/plt_*.ets_build/default/rebar3_*.pltThese paths point to where Rebar3 typically stores the generated PLT files. The exact paths might vary slightly based on Rebar3 versions or project configurations, but the goal is to cache the PLT files themselves. (Note: Elixir projects using Dialyxir often cache PLTs inpriv/plts/, but the principle is the same).
Cache Key (key): ${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock', '**/rebar.config') }} This key is similar to the Hex cache key but with an important addition:
${{ hashFiles('**/rebar.lock', '**/rebar.config') }}: It includes the hash ofrebar.configin addition torebar.lock. Therebar.configfile contains project definitions, dependency specifications, application lists, and potentially Dialyzer-specific options (likeplt_add_appsor custom Dialyzer flags). If any of these change—for example, a new application is added to the project, or a dependency that affects the overall type analysis changes—the PLT needs to be rebuilt. Includingrebar.configin the hash ensures that such changes invalidate the PLT cache, leading to a correct and up-to-date PLT. This is critical for Dialyzer's effectiveness, as using a stale PLT could lead to missed warnings or incorrect analysis.
Restore Keys (restore-keys): ${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}- This provides a similar fallback mechanism as the Hex cache, attempting to restore a PLT for the same OS and OTP version if an exact match isn't found.
Conditional Dependency Installation
- name: Install Dependencies
if: steps.hex-cache.outputs.cache-hit!= 'true'
run: rebar3 deps
This step leverages the output of the Hex cache restoration. The rebar3 deps command, which downloads and installs project dependencies, is only executed if the Hex package cache was not successfully restored (i.e., steps.hex-cache.outputs.cache-hit is not 'true'). This is a direct optimization resulting from effective caching: if dependencies are already cached, there's no need to fetch them again.
Table: Cache Configuration Deep Dive
To consolidate the caching logic, the following table details the configurations for both Hex and PLT caches:
| Cache Type | Cached Paths | Key Components | Rationale for Key Components | Restore Key Strategy |
|--------------|----------------------------------------------------------|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| Hex Packages | ~/.cache/rebar3/hex<br>~/.hex | runner.os, steps.setup-beam.outputs.otp-version, hashFiles('**/rebar.lock') | OS and OTP version for environment specificity. rebar.lock hash ensures cache reflects exact dependency versions. | Fallback to latest cache for same OS and OTP version. |
| Dialyzer PLT | ~/.cache/rebar3/plt_*.ets<br>_build/default/rebar3_*.plt | runner.os, steps.setup-beam.outputs.otp-version, hashFiles('**/rebar.lock', '**/rebar.config') | OS and OTP for environment. rebar.lock for dependency changes. rebar.config hash for changes in project structure, applications, or Dialyzer settings that would necessitate a PLT rebuild for correctness. | Fallback to latest cache for same OS and OTP version. |
This detailed caching strategy is a hallmark of an optimized CI pipeline, balancing the need for speed with the imperative of correctness.
The Erlang Build & Test Gauntlet: Ensuring Code Quality with Rebar3
Once the environment is set up and caches are restored, the core build and test operations begin, primarily orchestrated by Rebar3, the standard build tool for Erlang projects.rebar3 deps (Conditional)
As discussed in the caching section, rebar3 deps is run conditionally. If executed, it resolves and fetches all project dependencies defined in rebar.config and rebar.lock.
Compile
- name: Compile
run: rebar3 compile
This is a fundamental Rebar3 command that compiles all Erlang source code (.erl files) and application resource files (.app.src) within the project and its dependencies, producing BEAM bytecode files (.beam). A successful compilation is the first gatekeeper for code quality.
Run Dialyzer
The rebar3 dialyzer command invokes Dialyzer, Erlang's powerful static analysis tool. Dialyzer examines the compiled bytecode for type inconsistencies, unreachable code, and other potential issues without actually executing the code. It relies on type specifications (
-spec attributes) provided by developers and can infer types to some extent. Running Dialyzer in CI is a hallmark of high-quality Erlang development, as it helps catch a class of errors that might be missed by dynamic testing or compiler checks alone, contributing significantly to the robustness of the final application.
Run EUnit Tests with Coverage
- name: Run EUnit tests with coverage
run: rebar3 eunit --cover
EUnit is Erlang’s built-in unit testing framework, typically used for testing individual functions and modules in isolation. Therebar3 eunit command discovers and executes these tests.
- The
--coverflag is crucial here. It instructs Rebar3 to compile the relevant modules with coverage analysis enabled and then, after the tests run, to generate coverage data (.coverdatafiles). This data indicates which lines of code were executed by the EUnit tests.
Run Common Test Suites with Coverage
- name: Run Common Test suites with coverage
run: rebar3 ct --cover
Common Test is Erlang/OTP’s framework for more complex, scenario-based testing, often used for integration tests, system tests, and property-based testing. The
rebar3 ct command discovers and executes Common Test suites (typically files named *_SUITE.erl).
- Similar to EUnit, the
--coverflag enables the generation of coverage data for code exercised by the Common Test suites.
The inclusion of both EUnit and Common Test, each with coverage enabled, demonstrates a comprehensive testing strategy. EUnit handles fine-grained unit verification, while Common Test addresses broader component interactions and system behaviors. Enabling coverage for both ensures that the final code coverage report reflects the combined testing effort across different granularities. This dual-pronged approach is a strong feature of a “complete” CI pipeline.
Measuring Quality: Code Coverage with rebar3_codecov and Codecov.io
Running tests is essential, but understanding what those tests cover is equally important. Code coverage metrics provide insights into how much of the codebase is exercised by the test suites. This workflow uses the rebar3_codecov plugin and the codecov/codecov-action to process and upload coverage data to Codecov.io.
Generating Codecov JSON Report (using rebar3_codecov)
- name: Generate Codecov JSON report (using rebar3_codecov)
# Executa sempre para que os artefatos sejam gerados mesmo se os testes falharem.
if: always()
run: |
rebar3 as test codecov analyze |
| echo "Codecov analysis failed but continuing"
This step is responsible for converting the raw .coverdata files (generated by eunit --cover and ct --cover) into a JSON format that Codecov.io can understand.
**rebar3 as test codecov analyze**: This command invokes therebar3_codecovplugin. Theas testpart ensures that the command runs under thetestprofile, which might be necessary if the plugin itself has test-specific dependencies or configurations. Thecodecov analyzesubcommand processes the coverdata and typically outputs acodecov.jsonfile.**if: always()**: This condition ensures that this step runs regardless of the success or failure of previous steps (like the test execution steps). Even if some tests fail, there might still be partial coverage data that could be useful for analysis.**|| echo "Codecov analysis failed but continuing": This shell construct makes the step non-fatal. If therebar3 as test codecov analyzecommand fails for any reason (e.g., no coverdata files found, an issue with the plugin itself), theechocommand will execute, and the step will still be considered successful by GitHub Actions**. This prevents a failure in coverage report generation from halting the entire workflow, allowing subsequent steps like artifact uploading to proceed. This resilience is a pragmatic choice for a complex CI pipeline.
Uploading Coverage to Codecov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
# Condição para enviar o relatório apenas uma vez, usando uma versão específica do OTP.
if: success() && matrix.otp == '27.0' # Note: The YAML uses '27.0', which might be a specific target.
# The matrix includes '27.3.4'. Assuming '27.0' is a placeholder or a broader match.
# For precision, it should match an exact matrix.otp value like '27.3.4'.
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: _build/test/codecov/codecov.json
fail_ci_if_error: true
verbose: true
This step uses the official codecov/codecov-action@v4 to upload the generated codecov.json report to Codecov.io.
**if: success() && matrix.otp == '27.0'**: This conditional logic is a key optimization and best practice for matrix builds.
success(): Ensures that coverage data is only uploaded if all preceding steps in the current job (compile, Dialyzer, EUnit, Common Test) have passed. Uploading coverage from a failed build can be misleading.matrix.otp == '27.0'(or a similar specific version from the matrix like'27.3.4'): This condition ensures that the coverage report is uploaded only once per commit, from a single, designated job run in the matrix. OTP 27.x is often chosen as a representative modern Erlang version. Code coverage for the core application logic should ideally be consistent across different OTP versions (assuming the tests themselves are portable). Uploading from every job in the matrix would create redundant reports in Codecov, consume upload quotas, and clutter the coverage history. This strategic upload ensures that the reported coverage reflects a successful build on a representative Erlang version and is a hallmark of an advanced, well-thought-out CI pipeline.
Inputs (with):
token: ${{ secrets.CODECOV_TOKEN }}: This securely passes the Codecov upload token to the action. The token should be stored as an encrypted secret (namedCODECOV_TOKEN) in the GitHub repository settings.files: _build/test/codecov/codecov.json: Specifies the path to the JSON coverage report generated byrebar3_codecov.fail_ci_if_error: true: If the Codecov action encounters an error during the upload (e.g., invalid token, Codecov service interruption), the CI job will be marked as failed. This is important if maintaining up-to-date coverage reports is considered a critical part of the CI process.verbose: true: Enables more detailed logging output from the Codecov action, which can be helpful for troubleshooting upload issues.
Table: codecov/codecov-action@v4 Inputs Explained
The configuration of the codecov/codecov-action is clarified in the table below:
| Input Parameter | Value from Workflow | Purpose/Explanation |
|------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| token | ${{ secrets.CODECOV_TOKEN }} | The authentication token for uploading reports to Codecov.io. Must be configured as a repository secret. |
| files | _build/test/codecov/codecov.json | Comma-separated list of coverage report files to upload. Here, it points to the JSON file generated by rebar3_codecov. |
| fail_ci_if_error | true | If set to true, the GitHub Actions step will fail if the Codecov CLI encounters an error during upload. |
| verbose | true | Enables verbose logging from the Codecov CLI, which can aid in debugging issues with the upload process. |
This setup ensures that code coverage is consistently tracked and reported, providing valuable feedback to the development team.
Preserving Evidence: Managing Build Artifacts
Beyond pass/fail status and coverage reports, CI workflows often generate other useful files, such as detailed test logs or HTML versions of coverage reports. GitHub Actions allows these files to be saved as “artifacts,” which can be downloaded for later inspection.
Uploading Test Logs and HTML Coverage Report
- name: Upload test logs and HTML coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: Erlang-OTP-${{ matrix.otp }}-Rebar3-${{ matrix.rebar3 }}
path: |
_build/test/logs/
_build/test/cover/
if-no-files-found: warn
retention-days: 7
This step uses the actions/upload-artifact@v4 action to package and save specified files and directories.
**if: always()**: This condition is critical. It ensures that artifacts are uploaded even if previous steps in the job (like tests) have failed. For debugging purposes, having access to logs and any partially generated reports from a failed run is invaluable. If artifacts were only uploaded on success, this crucial diagnostic information would be lost.
Inputs (with):
name: Erlang-OTP-${{ matrix.otp }}-Rebar3-${{ matrix.rebar3 }}: This dynamically constructs a unique name for the artifact based on the current OTP and Rebar3 versions from the matrix. For example, an artifact might be named "Erlang-OTP-25.3.2.21-Rebar3-3.24.0". This makes it extremely easy to locate and download the correct set of logs or reports corresponding to a specific matrix job run directly from the GitHub Actions UI.
path: | _build/test/logs/ _build/test/cover/: This specifies the files and directories to include in the artifact.
_build/test/logs/: This directory typically contains detailed logs generated by Common Test._build/test/cover/: This directory often contains HTML versions of the code coverage report generated byrebar3 cover, which can be browsed locally.
if-no-files-found: warn: If the specified paths do not contain any files (e.g., if tests were skipped or did not produce any output), the action will output a warning but will not fail the job. This is a sensible default.
retention-days: 7: This specifies that the uploaded artifacts should be stored by GitHub for 7 days. The default retention period is 90 days if not specified. A shorter period like 7 days can be a good balance for active projects to manage storage usage, while still providing ample time for accessing recent build artifacts.
The combination of if: always() and dynamically named artifacts for each matrix job creates a powerful debugging aid. If a particular OTP/Rebar3 combination fails, developers can directly download the relevant logs and coverage reports for that exact environment to diagnose the issue.
Table: actions/upload-artifact@v4 Inputs Explained
The parameters for the actions/upload-artifact@v4 action are detailed below:
| Input Parameter | Value from Workflow | Purpose/Explanation |
|-------------------|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | Erlang-OTP-${{ matrix.otp }}-Rebar3-${{ matrix.rebar3 }} | The name of the artifact to be uploaded. Dynamic naming based on matrix variables helps in easily identifying artifacts from specific job runs. |
| path | _build/test/logs/<br>_build/test/cover/ | A list of files, directories, or wildcard patterns to include in the artifact. Supports multi-line input for multiple paths. |
| if-no-files-found | warn | Defines the behavior if no files are found at the specified path(s). Options are warn, error, or ignore. warn is the default and usually appropriate. |
| retention-days | 7 | The number of days to retain the artifact. If not specified, defaults to repository/organization settings (typically 90 days). |
This artifact management strategy ensures that valuable diagnostic information is preserved and easily accessible, further enhancing the utility of the CI pipeline.
The Full Picture: The Complete ubunto_workflow_matrix.yml
The preceding sections have meticulously dissected each component of the Erlang CI workflow. For completeness and ease of use, the entire ubunto_workflow_matrix.yml file is provided below. This file can be adapted and integrated into Erlang projects to establish a robust and comprehensive CI pipeline on GitHub Actions.
name: Erlang CI on Ubuntu
on: [push, pull_request]
permissions:
contents: read # Necessário para actions/checkout
jobs:
build:
# Nome do job customizado para exibição mais clara no GitHub Actions UI
name: OTP ${{ matrix.otp }} Rebar3 ${{ matrix.rebar3 }}
runs-on: ubuntu-24.04
strategy:
# Garante que todos os jobs na matriz rodem, mesmo que um falhe.
fail-fast: false
matrix:
# Define as combinações específicas de OTP e Rebar3 que devem ser executadas.
# Adicione aqui apenas as versões que você sabe que são compatíveis.
include:
# rebar3 3.24.0 é compatível com OTP 25-27
- otp: '25.3.2.21'
rebar3: '3.24.0'
- otp: '26.2.5.12'
rebar3: '3.24.0'
- otp: '27.3.4'
rebar3: '3.24.0'
# rebar3 3.25.0 é compatível com OTP 26-28
- otp: '26.2.5.12'
rebar3: '3.25.0'
- otp: '27.3.4'
rebar3: '3.25.0'
- otp: '28.0'
rebar3: '3.25.0'
# Adicionadas versões compatíveis para OTP 24.3.4.17
# rebar3 3.21.0, 3.20.0 e 3.19.0 são compatíveis com OTP 24
- otp: '24.3.4.17'
rebar3: '3.21.0'
- otp: '24.3.4.17'
rebar3: '3.20.0'
- otp: '24.3.4.17'
rebar3: '3.19.0'
#env:
# A versão do rebar3 agora é definida por cada par na matriz 'include'.
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up BEAM
uses: erlef/setup-beam@v1
id: setup-beam
with:
otp-version: ${{ matrix.otp }}
# Agora usa a versão do rebar3 da matriz
rebar3-version: ${{ matrix.rebar3 }}
- name: Restore Hex package cache
uses: actions/cache@v4
id: hex-cache
with:
path: |
~/.cache/rebar3/hex
~/.hex
key: ${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock') }}
restore-keys: |
${{ runner.os }}-hex-${{ steps.setup-beam.outputs.otp-version }}-
- name: Restore Dialyzer PLT cache
uses: actions/cache@v4
id: plt-cache
with:
path: |
~/.cache/rebar3/plt_*.ets
_build/default/rebar3_*.plt
key: ${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}-${{ hashFiles('**/rebar.lock', '**/rebar.config') }}
restore-keys: |
${{ runner.os }}-plt-${{ steps.setup-beam.outputs.otp-version }}-
- name: Install Dependencies
if: steps.hex-cache.outputs.cache-hit!= 'true'
run: rebar3 deps
- name: Compile
run: rebar3 compile
- name: Run Dialyzer
run: rebar3 dialyzer
- name: Run EUnit tests with coverage
run: rebar3 eunit --cover
- name: Run Common Test suites with coverage
run: rebar3 ct --cover
- name: Generate Codecov JSON report (using rebar3_codecov)
# Executa sempre para que os artefatos sejam gerados mesmo se os testes falharem.
if: always()
run: |
rebar3 as test codecov analyze |
| echo "Codecov analysis failed but continuing"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
# Condição para enviar o relatório apenas uma vez, usando uma versão específica do OTP.
if: success() && matrix.otp == '27.0' # Adjust '27.0' to match a specific version in your matrix, e.g., '27.3.4'
with:
# Esta linha usa o segredo que você deve configurar no repositório.
token: ${{ secrets.CODECOV_TOKEN }}
files: _build/test/codecov/codecov.json
fail_ci_if_error: true
verbose: true
- name: Upload test logs and HTML coverage report
if: always()
uses: actions/upload-artifact@v4
with:
# Nome do artefato atualizado para ser mais apresentável
name: Erlang-OTP-${{ matrix.otp }}-Rebar3-${{ matrix.rebar3 }}
path: |
_build/test/logs/
_build/test/cover/
if-no-files-found: warn
retention-days: 7
Conclusion: Elevate Your Erlang CI to the Next Level
This guide has walked through a sophisticated Erlang CI workflow for GitHub Actions, highlighting the features that contribute to its completeness and robustness. Key strengths include:
- Comprehensive Multi-Version Testing: The matrix strategy with
fail-fast: falseand explicitincludecombinations ensures thorough validation across a defined range of Erlang/OTP and Rebar3 versions. - Efficient Caching: Intelligent caching of Hex packages and Dialyzer PLTs dramatically reduces build times by reusing previously fetched or generated data.
- Static Analysis: Integration of Dialyzer provides an essential layer of static type checking, catching potential errors before runtime.
- Thorough Test Execution: The pipeline runs both EUnit and Common Test suites, covering unit-level and integration/system-level testing, with coverage enabled for both.
- Code Coverage Reporting: Automated generation and upload of coverage reports to Codecov.io offer clear visibility into test effectiveness and help identify untested code paths.
- Excellent Artifact Management: Test logs and HTML coverage reports are archived for each matrix job, regardless of success or failure, providing crucial data for debugging and analysis.
While this workflow is comprehensive, the journey of CI/CD is one of continuous improvement. As projects evolve, developers might consider further enhancements such as:
- Automated Release Packaging and Deployment: Extending the CI to build Erlang releases and deploy them to staging or production environments.
- Security Scanning: Integrating tools to scan dependencies for known vulnerabilities (e.g.,
mix_auditfor Elixir has Erlang applications, or other general-purpose scanners). - Performance Testing: Adding steps to run performance benchmarks and track regressions.
- More Complex Notification Systems: Setting up custom notifications for build successes or failures to specific channels or services.
By adopting and adapting the principles and configurations outlined in this guide, Erlang developers can significantly elevate their CI practices. Implementing such a workflow will lead to higher code quality, faster feedback cycles, increased development velocity, and greater confidence in the stability and reliability of their Erlang applications.
Of course. Here is the reference list translated into English.
References for the Erlang Continuous Integration Article
To support the article on the CI/CD pipeline for Erlang projects using GitHub Actions, below is a curated list of references. These sources provide the official documentation and technical context for each component and tool used in the ubunto_workflow_matrix.yml file.
GitHub Actions
- Official GitHub Actions Documentation: The starting point for understanding the syntax, concepts of workflows, jobs, steps, and the matrix strategy.
Marketplace: erlef/setup-beam: The essential action for setting up the Erlang/OTP, Elixir, and Rebar3 environment on the GitHub Actions runner. The documentation details all of its input options, such as otp-version and rebar3-version.
- erlef/setup-beam on GitHub Marketplace
[erlef/setup-beamRepository](https://www.google.com/search?q=%5Bhttps://github.com/erlef/setup-beam%5D(https://github.com/erlef/setup-beam))
Marketplace: actions/cache: Official documentation for the cache action, explaining how to store and restore dependencies and other files to speed up workflows. Crucial for understanding the configuration of cache keys.
[actions/cacheRepository](https://www.google.com/search?q=%5Bhttps://github.com/actions/cache%5D(https://github.com/actions/cache))
Marketplace: actions/upload-artifact: Documentation for the action that allows uploading files (artifacts) generated during the workflow execution, such as coverage reports and test logs.
[actions/upload-artifactRepository](https://www.google.com/search?q=%5Bhttps://github.com/actions/upload-artifact%5D(https://github.com/actions/upload-artifact))
Build and Test Tools (Erlang)
Official Rebar3 Documentation: The complete manual for Erlang’s standard build tool. It contains detailed information on all commands used in the CI, such as compile, eunit, ct, and dialyzer.
- Rebar3 Documentation
[erlang/rebar3Repository](https://www.google.com/search?q=%5Bhttps://github.com/erlang/rebar3%5D(https://github.com/erlang/rebar3))
- Official Erlang/OTP Documentation: The primary source for the entire Erlang platform, including usage guides, reference manuals, and OTP design principles.
- Erlang/OTP Documentation
- Erlang/OTP Downloads Page (useful for checking available versions).
Code Coverage (Codecov)
**rebar3_codecov Plugin:** The repository for the Rebar3 plugin that converts test coverage data (.coverdata) to the JSON format compatible with the Codecov platform.
[esl/rebar3_codecov Repository](https://www.google.com/search?q=%5Bhttps://github.com/esl/rebar3_codecov%5D(https://github.com/esl/rebar3_codecov))
Marketplace: codecov/codecov-action: The official Codecov action for uploading coverage reports to the platform from GitHub Actions. The documentation explains inputs like token, files, and fail_ci_if_error.
Codecov Documentation: General guides on how to integrate Codecov with your repository, including how to obtain and configure the CODECOV_TOKEN.
메타데이터
- post_id
- 3342ddf9d0bf
- slug
- crafting-the-definitive-erlang-ci-pipeline-with-github-actions-a-comprehensive-guide-3342ddf9d0bf
- url
- https://medium.com/@matheuscamarques/crafting-the-definitive-erlang-ci-pipeline-with-github-actions-a-comprehensive-guide-3342ddf9d0bf
- canonical_url
- https://medium.com/@matheuscamarques/crafting-the-definitive-erlang-ci-pipeline-with-github-actions-a-comprehensive-guide-3342ddf9d0bf
- author_url
- https://medium.com/@matheuscamarques
- status
- ok
- fetched_at
- 2026-07-19 11:49:42