From 16274ea1e594d9356ec60cc99a322d07ea83014d Mon Sep 17 00:00:00 2001 From: Micheal Wilkinson Date: Sat, 21 Mar 2026 00:17:37 +0000 Subject: [PATCH] feat: add reusable coverage-badge action --- .gitea/workflows/push-validation.yml | 113 ++++++------------ README.md | 24 +++- changelog.md | 4 + coverage-badge/action.yml | 168 +++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 78 deletions(-) create mode 100644 coverage-badge/action.yml diff --git a/.gitea/workflows/push-validation.yml b/.gitea/workflows/push-validation.yml index a883657..3dd3cc9 100644 --- a/.gitea/workflows/push-validation.yml +++ b/.gitea/workflows/push-validation.yml @@ -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"; - }')" + echo 'Summary' + echo - cat > coverage-badge.svg < - - - - - - - - - - - - - - coverage - coverage - ${COVERAGE_TOTAL}% - ${COVERAGE_TOTAL}% - - - EOF + if [[ -s "$SUMMARY_FILE" ]]; then + cat "$SUMMARY_FILE" + else + echo 'No summary generated.' + fi - - name: Upload branch coverage artefacts - id: upload - run: | - set -euo pipefail + 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 - 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 - echo '- Total: `${{ steps.coverage.outputs.total }}%`' - echo '- Report: ${{ steps.upload.outputs.report_url }}' - echo '- Badge: ${{ steps.upload.outputs.badge_url }}' - } >> "$SUMMARY_FILE" + - 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 diff --git a/README.md b/README.md index 03dd35a..f591a98 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/changelog.md b/changelog.md index d490c5f..769a528 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/coverage-badge/action.yml b/coverage-badge/action.yml new file mode 100644 index 0000000..68f6daa --- /dev/null +++ b/coverage-badge/action.yml @@ -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" < + + + + + + + + + + + + + + coverage + coverage + ${total}% + ${total}% + + + 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"