Skip to content

Commit ff9587b

Browse files
authored
feat(reports): include file line in GitHub Actions annotations (#710)
1 parent 2017b47 commit ff9587b

4 files changed

Lines changed: 67 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
### Changed
1111
- `install.sh` now verifies the release checksum by default (set `BASHUNIT_VERIFY_CHECKSUM=false` to opt out); it soft-skips with a warning when a checksum asset is unavailable unless verification was explicitly requested (#703)
12+
- `--log-gha` annotations now include the failing test's `line` (`::error file=…,line=…`), so they pin to the exact line in a pull request's "Files changed" tab (#704)
1213

1314
## [0.38.0](https://github.com/TypedDevs/bashunit/compare/0.37.0...0.38.0) - 2026-06-07
1415

docs/command-line.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,12 @@ bashunit test tests/ --log-junit results.xml
361361
bashunit test tests/ --report-html report.html
362362
```
363363
```bash [GitHub Actions]
364-
bashunit test tests/ --log-gha gha.log && cat gha.log
364+
# Stream annotations straight to the runner log:
365+
bashunit test tests/ --log-gha /dev/stdout
365366
```
366367
:::
367368

368-
The `--log-gha` flag writes GitHub Actions workflow commands (`::error`, `::warning`, `::notice`) for failed, risky and incomplete tests. When streamed to stdout on a runner, they appear as inline annotations in the "Files changed" tab of a pull request.
369+
The `--log-gha` flag writes GitHub Actions workflow commands (`::error`, `::warning`, `::notice`) for failed, risky and incomplete tests, including the failing test's `file` and `line`. Point it at `/dev/stdout` (or stream a log file to stdout) on a runner and the failures appear as inline annotations in the "Files changed" tab of a pull request.
369370

370371
### Show Output on Failure
371372

src/reports.sh

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ _BASHUNIT_REPORTS_TEST_STATUSES=()
77
_BASHUNIT_REPORTS_TEST_DURATIONS=()
88
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
99
_BASHUNIT_REPORTS_TEST_FAILURES=()
10+
_BASHUNIT_REPORTS_TEST_LINES=()
1011

1112
function bashunit::reports::add_test_snapshot() {
1213
bashunit::reports::add_test "$1" "$2" "$3" "$4" "snapshot"
@@ -47,12 +48,21 @@ function bashunit::reports::add_test() {
4748
local status="$5"
4849
local failure_message="${6:-}"
4950

51+
# Capture the line number from the current test location ("file:line"),
52+
# but only when it belongs to this test's file, so a stale location from a
53+
# prior test never mislabels this entry.
54+
local line=""
55+
case "${_BASHUNIT_TEST_LOCATION:-}" in
56+
"$file":*) line="${_BASHUNIT_TEST_LOCATION##*:}" ;;
57+
esac
58+
5059
_BASHUNIT_REPORTS_TEST_FILES[${#_BASHUNIT_REPORTS_TEST_FILES[@]}]="$file"
5160
_BASHUNIT_REPORTS_TEST_NAMES[${#_BASHUNIT_REPORTS_TEST_NAMES[@]}]="$test_name"
5261
_BASHUNIT_REPORTS_TEST_STATUSES[${#_BASHUNIT_REPORTS_TEST_STATUSES[@]}]="$status"
5362
_BASHUNIT_REPORTS_TEST_ASSERTIONS[${#_BASHUNIT_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
5463
_BASHUNIT_REPORTS_TEST_DURATIONS[${#_BASHUNIT_REPORTS_TEST_DURATIONS[@]}]="$duration"
5564
_BASHUNIT_REPORTS_TEST_FAILURES[${#_BASHUNIT_REPORTS_TEST_FAILURES[@]}]="$failure_message"
65+
_BASHUNIT_REPORTS_TEST_LINES[${#_BASHUNIT_REPORTS_TEST_LINES[@]}]="$line"
5666
}
5767

5868
function bashunit::reports::__xml_escape() {
@@ -131,17 +141,18 @@ function bashunit::reports::__gha_encode() {
131141
printf '%s' "$text"
132142
}
133143

134-
function bashunit::reports::generate_gha_log() {
135-
local output_file="$1"
136-
137-
: >"$output_file"
144+
# Echoes GitHub Actions workflow-command annotations to stdout.
145+
# Arguments: $1 - "failed-only" to emit just errors (default: all reportable).
146+
function bashunit::reports::print_gha_annotations() {
147+
local only="${1:-all}"
138148

139149
local i
140150
for i in "${!_BASHUNIT_REPORTS_TEST_NAMES[@]}"; do
141151
local file="${_BASHUNIT_REPORTS_TEST_FILES[$i]:-}"
142152
local name="${_BASHUNIT_REPORTS_TEST_NAMES[$i]:-}"
143153
local status="${_BASHUNIT_REPORTS_TEST_STATUSES[$i]:-}"
144154
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
155+
local line="${_BASHUNIT_REPORTS_TEST_LINES[$i]:-}"
145156
local level="" message=""
146157

147158
case "$status" in
@@ -162,12 +173,27 @@ function bashunit::reports::generate_gha_log() {
162173
;;
163174
esac
164175

176+
if [ "$only" = "failed-only" ] && [ "$status" != "failed" ]; then
177+
continue
178+
fi
179+
180+
local location="file=${file}"
181+
if [ -n "$line" ]; then
182+
location="${location},line=${line}"
183+
fi
184+
165185
local encoded_message
166186
encoded_message=$(bashunit::reports::__gha_encode "$message")
167-
echo "::${level} file=${file},title=${name}::${encoded_message}" >>"$output_file"
187+
echo "::${level} ${location},title=${name}::${encoded_message}"
168188
done
169189
}
170190

191+
function bashunit::reports::generate_gha_log() {
192+
local output_file="$1"
193+
194+
bashunit::reports::print_gha_annotations all >"$output_file"
195+
}
196+
171197
function bashunit::reports::generate_report_html() {
172198
local output_file="$1"
173199

tests/unit/reports_test.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ function set_up() {
1414
_BASHUNIT_REPORTS_TEST_DURATIONS=()
1515
_BASHUNIT_REPORTS_TEST_ASSERTIONS=()
1616
_BASHUNIT_REPORTS_TEST_FAILURES=()
17+
_BASHUNIT_REPORTS_TEST_LINES=()
18+
_BASHUNIT_TEST_LOCATION=""
1719

1820
# Unset report env vars by default
1921
unset BASHUNIT_LOG_JUNIT
@@ -387,6 +389,36 @@ function test_generate_gha_log_skips_skipped_test() {
387389
assert_empty "$content"
388390
}
389391

392+
function test_generate_gha_log_includes_line_when_location_known() {
393+
_mock_state_functions
394+
BASHUNIT_LOG_GHA="gha.log"
395+
_BASHUNIT_TEST_LOCATION="tests/foo_test.sh:42"
396+
397+
bashunit::reports::add_test "tests/foo_test.sh" "test_fail" "100" "1" "failed" "boom"
398+
bashunit::reports::generate_gha_log "$_TEMP_OUTPUT_FILE"
399+
400+
local content
401+
content=$(cat "$_TEMP_OUTPUT_FILE")
402+
403+
assert_contains '::error file=tests/foo_test.sh,line=42,title=test_fail' "$content"
404+
}
405+
406+
function test_print_gha_annotations_failed_only_to_stdout() {
407+
_mock_state_functions
408+
BASHUNIT_LOG_GHA="gha.log"
409+
410+
bashunit::reports::add_test "tests/foo_test.sh" "test_ok" "100" "1" "passed"
411+
bashunit::reports::add_test "tests/foo_test.sh" "test_bad" "100" "1" "failed" "boom"
412+
bashunit::reports::add_test "tests/foo_test.sh" "test_risky" "10" "0" "risky"
413+
414+
local output
415+
output=$(bashunit::reports::print_gha_annotations failed-only)
416+
417+
assert_contains '::error file=tests/foo_test.sh' "$output"
418+
assert_not_contains '::warning' "$output"
419+
assert_not_contains 'test_risky' "$output"
420+
}
421+
390422
function test_generate_gha_log_encodes_newlines_in_message() {
391423
_mock_state_functions
392424
BASHUNIT_LOG_GHA="gha.log"

0 commit comments

Comments
 (0)