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: jobs:
validate: coverage-badge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: docker.io/catthehacker/ubuntu:act-latest container: docker.io/catthehacker/ubuntu:act-latest
defaults: defaults:
@@ -35,96 +35,57 @@ jobs:
cache: true cache: true
cache-dependency-path: go.sum 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 - name: Run full unit test suite with coverage
id: coverage
run: | run: |
set -euo pipefail set -euo pipefail
go test -covermode=atomic -coverprofile=coverage.out ./... 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}')" - name: Publish coverage badge artefacts
printf '{\n "total": "%s"\n}\n' "$total" > coverage-summary.json id: coverage
printf 'total=%s\n' "$total" >> "$GITHUB_OUTPUT" 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 - name: Summary
env: if: ${{ always() }}
COVERAGE_TOTAL: ${{ steps.coverage.outputs.total }}
run: | run: |
set -euo pipefail set -euo pipefail
color="$(awk -v total="$COVERAGE_TOTAL" 'BEGIN { echo 'Summary'
if (total >= 80) print "brightgreen"; echo
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 if [[ -s "$SUMMARY_FILE" ]]; then
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="coverage: ${COVERAGE_TOTAL}%"> cat "$SUMMARY_FILE"
<linearGradient id="smooth" x2="0" y2="100%"> else
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> echo 'No summary generated.'
<stop offset="1" stop-opacity=".1"/> fi
</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 recommend-release:
id: upload runs-on: ubuntu-latest
run: | container: docker.io/catthehacker/ubuntu:act-latest
set -euo pipefail 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
aws configure set default.s3.addressing_style path - name: Setup Go
uses: actions/setup-go@v5
repo_name="${GITHUB_REPOSITORY##*/}" with:
prefix="${repo_name}/branch/${GITHUB_REF_NAME}" go-version: '1.26.1'
display_endpoint="${ARTEFACT_BUCKET_ENDPONT#https://}" check-latest: true
display_endpoint="${display_endpoint#http://}" cache: true
report_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" cache-dependency-path: go.sum
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
echo '- Total: `${{ steps.coverage.outputs.total }}%`'
echo '- Report: ${{ steps.upload.outputs.report_url }}'
echo '- Badge: ${{ steps.upload.outputs.badge_url }}'
} >> "$SUMMARY_FILE"
- name: Recommend next release tag on main pushes - name: Recommend next release tag on main pushes
if: ${{ github.ref == 'refs/heads/main' }}
run: | run: |
set -euo pipefail set -euo pipefail

View File

@@ -16,8 +16,8 @@ revision.
## Use In Other Repositories ## Use In Other Repositories
Vociferate ships two composite actions that together cover the full release flow. 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 both actions to the same released tag. 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 ### `prepare` — update files, commit, and push tag
@@ -109,6 +109,26 @@ assets after it runs:
--data-binary "@dist/myapp" --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 ## Why The Name
> **vociferate** _(verb)_: to cry out loudly or forcefully. > **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. - 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. - 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 ### 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 ### Removed
### Fixed ### 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"