Skip to content

Commit 4b10e5d

Browse files
test: JUnit XML test results → Codecov Test Analytics + EnricoMi check (#21)
1 parent 4249084 commit 4b10e5d

7 files changed

Lines changed: 148 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ jobs:
104104
# under /srv/hyper, unavailable on a CI runner.
105105
run: mix coveralls.json --no-start --warnings-as-errors
106106

107+
# !cancelled() (not always()) uploads results on test PASS or FAIL -- the
108+
# point of Test Analytics is failure/flake history -- while still skipping
109+
# if the workflow was cancelled. junit_formatter writes the XML at end of
110+
# run regardless of pass/fail. report_type: test_results sends JUnit to
111+
# Test Analytics via codecov-action (the standalone test-results-action is
112+
# deprecated and pinned to Node 20).
113+
- name: Upload test results to Codecov
114+
if: ${{ !cancelled() }}
115+
uses: codecov/codecov-action@v7
116+
with:
117+
token: ${{ secrets.CODECOV_TOKEN }}
118+
files: _build/test/junit.xml
119+
flags: elixir
120+
report_type: test_results
121+
122+
# The artifact feeds the workflow_run publisher (test-results.yml), which
123+
# runs EnricoMi with a write token -- so the GitHub Check works even on
124+
# fork PRs, where this job's token is read-only.
125+
- name: Upload Elixir test results artifact
126+
if: ${{ !cancelled() }}
127+
uses: actions/upload-artifact@v7
128+
with:
129+
name: elixir-test-results
130+
path: _build/test/junit.xml
131+
107132
- name: Upload coverage to Codecov
108133
uses: codecov/codecov-action@v7
109134
with:
@@ -139,13 +164,38 @@ jobs:
139164
- name: clippy
140165
run: cargo clippy --all-targets --all-features -- -D warnings
141166

142-
- name: Install cargo-llvm-cov
167+
- name: Install cargo-llvm-cov and nextest
143168
uses: taiki-e/install-action@v2
144169
with:
145-
tool: cargo-llvm-cov
170+
tool: cargo-llvm-cov,nextest
146171

147172
- name: test + coverage
148-
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
173+
# `nextest` runs the suite while llvm-cov collects coverage AND nextest
174+
# writes target/nextest/ci/junit.xml (see .config/nextest.toml). One run,
175+
# two artifacts: lcov.info for coverage, junit.xml for Test Analytics.
176+
run: cargo llvm-cov nextest --profile ci --all-features --lcov --output-path lcov.info
177+
178+
# !cancelled() uploads on pass OR fail (see elixir job). The action runs at
179+
# repo root, so the path is fully qualified -- working-directory only
180+
# affects `run:` steps, not `uses:` actions. report_type: test_results
181+
# sends JUnit via codecov-action (test-results-action is deprecated/Node 20).
182+
- name: Upload test results to Codecov
183+
if: ${{ !cancelled() }}
184+
uses: codecov/codecov-action@v7
185+
with:
186+
token: ${{ secrets.CODECOV_TOKEN }}
187+
files: native/suidhelper/target/nextest/ci/junit.xml
188+
flags: rust
189+
report_type: test_results
190+
191+
# Repo-relative path: upload-artifact is an action, so the rust job's
192+
# `working-directory: native/suidhelper` default does NOT apply.
193+
- name: Upload Rust test results artifact
194+
if: ${{ !cancelled() }}
195+
uses: actions/upload-artifact@v7
196+
with:
197+
name: rust-test-results
198+
path: native/suidhelper/target/nextest/ci/junit.xml
149199

150200
- name: Upload coverage to Codecov
151201
uses: codecov/codecov-action@v7
@@ -155,3 +205,16 @@ jobs:
155205
files: native/suidhelper/lcov.info
156206
flags: rust
157207
fail_ci_if_error: true
208+
209+
# Uploads the triggering event payload so the workflow_run publisher
210+
# (test-results.yml) can map results back to the originating PR -- required
211+
# for PR comments on fork PRs. Tiny job, always runs.
212+
event_file:
213+
name: Upload event file
214+
runs-on: ubuntu-latest
215+
steps:
216+
- name: Upload
217+
uses: actions/upload-artifact@v7
218+
with:
219+
name: Event File
220+
path: ${{ github.event_path }}

.github/workflows/test-results.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Publishes a unified GitHub Check + PR comment from the JUnit XML produced by
2+
# the CI workflow. Runs via workflow_run (not in CI itself) so it executes in
3+
# the base-repo context with a write token -- making test annotations work on
4+
# fork PRs and Dependabot branches, where CI's own token is read-only.
5+
name: Test Results
6+
7+
on:
8+
workflow_run:
9+
workflows: ["CI"]
10+
types:
11+
- completed
12+
13+
# Least privilege: only this workflow gets write scopes, and it never checks
14+
# out untrusted PR code -- it only reads artifacts the CI run produced.
15+
permissions: {}
16+
17+
jobs:
18+
publish:
19+
name: Publish test results
20+
runs-on: ubuntu-latest
21+
# Publish on success OR failure (failing tests are the whole point); skip
22+
# when CI was cancelled or skipped.
23+
if: >-
24+
github.event.workflow_run.conclusion == 'success' ||
25+
github.event.workflow_run.conclusion == 'failure'
26+
permissions:
27+
checks: write
28+
pull-requests: write
29+
contents: read
30+
actions: read
31+
steps:
32+
# download-artifact@v8 fetches artifacts from ANOTHER run via run-id +
33+
# github-token (no third-party action needed). Each artifact lands in its
34+
# own subdir: artifacts/<name>/...
35+
- name: Download CI artifacts
36+
uses: actions/download-artifact@v8
37+
with:
38+
run-id: ${{ github.event.workflow_run.id }}
39+
github-token: ${{ github.token }}
40+
path: artifacts
41+
42+
- name: Publish test results
43+
uses: EnricoMi/publish-unit-test-result-action@v2
44+
with:
45+
# head_sha attaches the check to the PR's commit (not the default
46+
# branch). event_file/event_name let the action resolve the PR for
47+
# commenting -- both come from the workflow_run payload.
48+
commit: ${{ github.event.workflow_run.head_sha }}
49+
event_file: artifacts/Event File/event.json
50+
event_name: ${{ github.event.workflow_run.event }}
51+
files: "artifacts/**/*.xml"
52+
check_name: Test Results

config/config.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ if config_env() == :test do
3434
config :opentelemetry, traces_exporter: :none
3535
# No cluster formation during tests.
3636
config :libcluster, topologies: []
37+
38+
# JUnit XML for Codecov Test Analytics. A fixed report_dir (not the default
39+
# app_path) gives CI a stable path; automatic_create_dir? mkdirs it since
40+
# the formatter uses File.write! which won't create parents. include_* embed
41+
# the source file + line so Codecov can link failures back to the test.
42+
config :junit_formatter,
43+
report_dir: "_build/test",
44+
report_file: "junit.xml",
45+
print_report_file: true,
46+
include_filename?: true,
47+
include_file_line?: true,
48+
automatic_create_dir?: true
3749
end
3850

3951
config :hyper, Hyper.Img.Db.Repo,

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ defmodule Hyper.MixProject do
5858
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
5959
{:stream_data, "~> 1.0", only: [:dev, :test], runtime: false},
6060
{:excoveralls, "~> 0.18", only: :test, runtime: false},
61+
{:junit_formatter, "~> 3.4", only: :test, runtime: false},
6162
{:dialyxir, "~> 1.4", only: [:dev], runtime: false},
6263
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
6364
{:ecto_sql, "~> 3.13"},

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
3333
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
3434
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
35+
"junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
3536
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
3637
"libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"},
3738
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# nextest CI profile. The [profile.ci.junit] table makes `--profile ci` write
2+
# JUnit XML to target/nextest/ci/junit.xml, which Codecov Test Analytics ingests.
3+
# store-success-output/-failure-output keep captured stdio in the report so a
4+
# failing test's output is visible in Codecov.
5+
[profile.ci]
6+
# Retry once to dampen rare flakes without masking genuine failures.
7+
retries = 1
8+
9+
[profile.ci.junit]
10+
path = "junit.xml"
11+
store-success-output = false
12+
store-failure-output = true

test/test_helper.exs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
ExUnit.start()
1+
# JUnitFormatter writes JUnit XML (consumed by Codecov Test Analytics) as a side
2+
# effect of the normal test run. Listing formatters explicitly REPLACES the
3+
# defaults, so ExUnit.CLIFormatter must be named here to keep console output.
4+
ExUnit.start(formatters: [ExUnit.CLIFormatter, JUnitFormatter])

0 commit comments

Comments
 (0)