feat: add reusable coverage-badge action

This commit is contained in:
Micheal Wilkinson
2026-03-21 00:17:37 +00:00
parent 8d9cc33802
commit 16274ea1e5
4 changed files with 231 additions and 78 deletions

View File

@@ -8,7 +8,7 @@ on:
- "*"
jobs:
validate:
coverage-badge:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
defaults:
@@ -35,96 +35,57 @@ jobs:
cache: true
cache-dependency-path: go.sum
- name: Install AWS CLI v2
uses: ankurk91/install-aws-cli-action@v1
- name: Verify AWS CLI
run: aws --version
- name: Run full unit test suite with coverage
id: coverage
run: |
set -euo pipefail
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
total="$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/, "", $3); print $3}')"
printf '{\n "total": "%s"\n}\n' "$total" > coverage-summary.json
printf 'total=%s\n' "$total" >> "$GITHUB_OUTPUT"
- name: Publish coverage badge artefacts
id: coverage
uses: ./coverage-badge
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
summary-file: ${{ env.SUMMARY_FILE }}
- name: Generate coverage badge
env:
COVERAGE_TOTAL: ${{ steps.coverage.outputs.total }}
- name: Summary
if: ${{ always() }}
run: |
set -euo pipefail
color="$(awk -v total="$COVERAGE_TOTAL" 'BEGIN {
if (total >= 80) print "brightgreen";
else if (total >= 70) print "green";
else if (total >= 60) print "yellowgreen";
else if (total >= 50) print "yellow";
else print "red";
}')"
cat > coverage-badge.svg <<EOF
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="coverage: ${COVERAGE_TOTAL}%">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="round">
<rect width="126" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#round)">
<rect width="63" height="20" fill="#555"/>
<rect x="63" width="63" height="20" fill="${color}"/>
<rect width="126" height="20" fill="url(#smooth)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="32.5" y="14">coverage</text>
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">${COVERAGE_TOTAL}%</text>
<text x="93.5" y="14">${COVERAGE_TOTAL}%</text>
</g>
</svg>
EOF
- name: Upload branch coverage artefacts
id: upload
run: |
set -euo pipefail
aws configure set default.s3.addressing_style path
repo_name="${GITHUB_REPOSITORY##*/}"
prefix="${repo_name}/branch/${GITHUB_REF_NAME}"
display_endpoint="${ARTEFACT_BUCKET_ENDPONT#https://}"
display_endpoint="${display_endpoint#http://}"
report_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html"
badge_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg"
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage.html "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-badge.svg "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp coverage-summary.json "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-summary.json" --content-type application/json
printf 'report_url=%s\n' "$report_url" >> "$GITHUB_OUTPUT"
printf 'badge_url=%s\n' "$badge_url" >> "$GITHUB_OUTPUT"
- name: Add coverage summary
run: |
set -euo pipefail
{
echo '## Coverage'
echo 'Summary'
echo
echo '- Total: `${{ steps.coverage.outputs.total }}%`'
echo '- Report: ${{ steps.upload.outputs.report_url }}'
echo '- Badge: ${{ steps.upload.outputs.badge_url }}'
} >> "$SUMMARY_FILE"
if [[ -s "$SUMMARY_FILE" ]]; then
cat "$SUMMARY_FILE"
else
echo 'No summary generated.'
fi
recommend-release:
runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest
needs: coverage-badge
if: ${{ github.ref == 'refs/heads/main' }}
defaults:
run:
shell: bash
env:
SUMMARY_FILE: ${{ runner.temp }}/push-validation-recommend-summary.md
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'
check-latest: true
cache: true
cache-dependency-path: go.sum
- name: Recommend next release tag on main pushes
if: ${{ github.ref == 'refs/heads/main' }}
run: |
set -euo pipefail

View File

@@ -16,8 +16,8 @@ revision.
## Use In Other Repositories
Vociferate ships two composite actions that together cover the full release flow.
Until release tags are created, reference `@main`. Once tags exist again, pin both actions to the same released tag.
Vociferate ships three composite actions covering release preparation, release publication, and coverage badge publishing.
Until release tags are created, reference `@main`. Once tags exist again, pin all actions to the same released tag.
### `prepare` — update files, commit, and push tag
@@ -109,6 +109,26 @@ assets after it runs:
--data-binary "@dist/myapp"
```
### `coverage-badge` - publish coverage report and badge
Run your coverage tests first, then call the action to generate `coverage.html`, `coverage-badge.svg`, and `coverage-summary.json`, upload them to S3-compatible storage, and emit output URLs.
```yaml
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@main
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Print coverage links
run: |
echo "Report: ${{ steps.coverage.outputs.report-url }}"
echo "Badge: ${{ steps.coverage.outputs.badge-url }}"
```
## Why The Name
> **vociferate** _(verb)_: to cry out loudly or forcefully.

View File

@@ -27,9 +27,13 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
- Added a project LICENSE file.
- Root and prepare actions now read `${{ vars.VOCIFERATE_REPOSITORY_URL }}` and forward it to `VOCIFERATE_REPOSITORY_URL` for repository URL override.
- Added a published `coverage-badge` composite action for generating and uploading coverage report/badge artefacts for reuse across repositories.
### Changed
- Push validation now handles coverage artefact and badge generation in a dedicated `coverage-badge` job, with release recommendation isolated in a separate dependent job.
- Push validation now calls the reusable `./coverage-badge` composite action for coverage badge generation and publication.
### Removed
### Fixed

168
coverage-badge/action.yml Normal file
View File

@@ -0,0 +1,168 @@
name: vociferate/coverage-badge
description: >
Generate coverage report artefacts, publish them to object storage,
and expose report URLs for workflow summaries.
inputs:
coverage-profile:
description: Path to the Go coverage profile file.
required: false
default: coverage.out
coverage-html:
description: Output path for the rendered HTML coverage report.
required: false
default: coverage.html
coverage-badge:
description: Output path for the generated SVG badge.
required: false
default: coverage-badge.svg
coverage-summary:
description: Output path for the generated coverage summary JSON.
required: false
default: coverage-summary.json
artefact-bucket-name:
description: S3 bucket name for published coverage artefacts.
required: true
artefact-bucket-endpoint:
description: Endpoint URL used for S3-compatible uploads.
required: true
branch-name:
description: Branch name used in the publication path.
required: false
default: ''
repository-name:
description: Repository name used in the publication path.
required: false
default: ''
summary-file:
description: Optional file path to append markdown summary output.
required: false
default: ''
outputs:
total:
description: Computed coverage percentage.
value: ${{ steps.generate.outputs.total }}
report-url:
description: Browser-facing URL for the published coverage report.
value: ${{ steps.upload.outputs.report_url }}
badge-url:
description: Browser-facing URL for the published coverage badge.
value: ${{ steps.upload.outputs.badge_url }}
runs:
using: composite
steps:
- name: Install AWS CLI v2
uses: ankurk91/install-aws-cli-action@v1
- name: Verify AWS CLI
shell: bash
run: aws --version
- name: Generate coverage artefacts
id: generate
shell: bash
env:
COVERAGE_PROFILE: ${{ inputs.coverage-profile }}
COVERAGE_HTML: ${{ inputs.coverage-html }}
COVERAGE_BADGE: ${{ inputs.coverage-badge }}
COVERAGE_SUMMARY: ${{ inputs.coverage-summary }}
run: |
set -euo pipefail
go tool cover -html="$COVERAGE_PROFILE" -o "$COVERAGE_HTML"
total="$(go tool cover -func="$COVERAGE_PROFILE" | awk '/^total:/ {sub(/%/, "", $3); print $3}')"
printf '{\n "total": "%s"\n}\n' "$total" > "$COVERAGE_SUMMARY"
printf 'total=%s\n' "$total" >> "$GITHUB_OUTPUT"
color="$(awk -v total="$total" 'BEGIN {
if (total >= 80) print "brightgreen";
else if (total >= 70) print "green";
else if (total >= 60) print "yellowgreen";
else if (total >= 50) print "yellow";
else print "red";
}')"
cat > "$COVERAGE_BADGE" <<EOF
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="coverage: ${total}%">
<linearGradient id="smooth" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="round">
<rect width="126" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#round)">
<rect width="63" height="20" fill="#555"/>
<rect x="63" width="63" height="20" fill="${color}"/>
<rect width="126" height="20" fill="url(#smooth)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="32.5" y="14">coverage</text>
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">${total}%</text>
<text x="93.5" y="14">${total}%</text>
</g>
</svg>
EOF
- name: Upload coverage artefacts
id: upload
shell: bash
env:
ARTEFACT_BUCKET_NAME: ${{ inputs.artefact-bucket-name }}
ARTEFACT_BUCKET_ENDPONT: ${{ inputs.artefact-bucket-endpoint }}
INPUT_BRANCH_NAME: ${{ inputs.branch-name }}
INPUT_REPOSITORY_NAME: ${{ inputs.repository-name }}
COVERAGE_HTML: ${{ inputs.coverage-html }}
COVERAGE_BADGE: ${{ inputs.coverage-badge }}
COVERAGE_SUMMARY: ${{ inputs.coverage-summary }}
run: |
set -euo pipefail
aws configure set default.s3.addressing_style path
branch_name="$(printf '%s' "$INPUT_BRANCH_NAME" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ -z "$branch_name" ]]; then
branch_name="${GITHUB_REF_NAME}"
fi
repo_name="$(printf '%s' "$INPUT_REPOSITORY_NAME" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
if [[ -z "$repo_name" ]]; then
repo_name="${GITHUB_REPOSITORY##*/}"
fi
prefix="${repo_name}/branch/${branch_name}"
display_endpoint="${ARTEFACT_BUCKET_ENDPONT#https://}"
display_endpoint="${display_endpoint#http://}"
report_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html"
badge_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg"
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_HTML" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_BADGE" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_SUMMARY" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-summary.json" --content-type application/json
printf 'report_url=%s\n' "$report_url" >> "$GITHUB_OUTPUT"
printf 'badge_url=%s\n' "$badge_url" >> "$GITHUB_OUTPUT"
- name: Append coverage summary
if: ${{ inputs.summary-file != '' }}
shell: bash
env:
SUMMARY_FILE: ${{ inputs.summary-file }}
TOTAL: ${{ steps.generate.outputs.total }}
REPORT_URL: ${{ steps.upload.outputs.report_url }}
BADGE_URL: ${{ steps.upload.outputs.badge_url }}
run: |
set -euo pipefail
{
echo '## Coverage'
echo
echo "- Total: \`${TOTAL}%\`"
echo "- Report: ${REPORT_URL}"
echo "- Badge: ${BADGE_URL}"
} >> "$SUMMARY_FILE"