feat: add decorate-pr composite action for pull request review decoration

This commit is contained in:
Micheal Wilkinson
2026-03-21 12:48:46 +00:00
parent 2810d93b89
commit 821802c0c4
3 changed files with 279 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ Published composite actions:
- `git.hrafn.xyz/aether/vociferate/prepare@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/publish@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.1`
## Action Selection Matrix
@@ -20,6 +21,7 @@ Use this when deciding which action to call:
- Choose `prepare` when you need to update changelog/version files, commit, and push a release tag.
- Choose `publish` when a tag already exists and you need to create or update release notes/assets.
- Choose `coverage-badge` after tests have produced `coverage.out` and you need coverage artefacts uploaded.
- Choose `decorate-pr` to annotate pull requests with coverage information and unreleased changelog entries.
- Choose root `vociferate` for direct recommend/prepare logic without commit/tag/push behavior.
## Preconditions
@@ -139,6 +141,34 @@ jobs:
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
```
### 4. Decorate Pull Request With Coverage and Changes
```yaml
jobs:
coverage:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_SECRET }}
AWS_DEFAULT_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_EC2_METADATA_DISABLED: true
steps:
- uses: actions/checkout@v4
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: badge
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Decorate PR
uses: git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.0
with:
coverage-percentage: ${{ steps.badge.outputs.total }}
badge-url: ${{ steps.badge.outputs.badge-url }}
```
## Inputs And Outputs Cheatsheet
### prepare
@@ -186,6 +216,24 @@ Primary outputs:
- `report-url`
- `badge-url`
### decorate-pr
Required inputs:
- `coverage-percentage` (0-100, typically from coverage-badge action)
- `badge-url` (SVG badge URL, typically from coverage-badge action)
Useful optional inputs:
- `changelog` (default `CHANGELOG.md`)
- `comment-title` (default `Vociferate Review`)
- `token` (defaults to workflow token)
Primary outputs:
- `comment-id`
- `comment-url`
## Guardrails For Agents
Use these rules to avoid common automation mistakes:

View File

@@ -16,7 +16,7 @@ revision.
## Use In Other Repositories
Vociferate ships three composite actions covering release preparation, release publication, and coverage badge publishing.
Vociferate ships composite actions covering release preparation, release publication, coverage badge publishing, and pull request decoration.
Release tags now exist; pin all action and reusable-workflow references to the same released tag (for example, `@v1.0.1`) instead of `@main`.
For agentic coding partners, see [`AGENTS.md`](AGENTS.md) for a direct integration playbook, selection matrix, and copy-paste workflow patterns.
@@ -131,6 +131,30 @@ Run your coverage tests first, then call the action to generate `coverage.html`,
echo "Badge: ${{ steps.coverage.outputs.badge-url }}"
```
### `decorate-pr` - annotate pull requests with coverage and changes
Decorate pull requests with coverage badges, coverage percentages, and unreleased changelog entries. The action creates a new comment or updates an existing one on each run.
```yaml
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Decorate pull request
if: github.event_name == 'pull_request'
uses: git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.0
with:
coverage-percentage: ${{ steps.coverage.outputs.total }}
badge-url: ${{ steps.coverage.outputs.badge-url }}
```
The action automatically finds existing vociferate comments by their marker and updates them instead of creating duplicates. This keeps PR timelines clean while keeping review information current.
## Why The Name
> **vociferate** _(verb)_: to cry out loudly or forcefully.

206
decorate-pr/action.yml Normal file
View File

@@ -0,0 +1,206 @@
name: vociferate/decorate-pr
description: >
Decorate pull requests with coverage badges, unreleased changelog entries,
and other useful review information. Updates existing comment or creates a
new one if it doesn't exist.
inputs:
coverage-percentage:
description: >
Computed coverage percentage (0-100). Typically from coverage-badge
action outputs.
required: true
badge-url:
description: >
Browser-facing URL for the coverage badge image (SVG). Typically from
coverage-badge action outputs.
required: true
changelog:
description: Path to changelog file relative to repository root.
required: false
default: CHANGELOG.md
comment-title:
description: >
Title/identifier for the PR comment. Used to locate existing comment
for updates. Defaults to 'Vociferate Review'.
required: false
default: 'Vociferate Review'
token:
description: >
GitHub or Gitea token for posting PR comments. Defaults to the
workflow token.
required: false
default: ''
outputs:
comment-id:
description: Numeric ID of the posted or updated PR comment.
value: ${{ steps.post-comment.outputs.comment_id }}
comment-url:
description: URL to the posted or updated PR comment.
value: ${{ steps.post-comment.outputs.comment_url }}
runs:
using: composite
steps:
- name: Validate PR context
id: validate
shell: bash
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
echo "This action only works on pull_request events" >&2
exit 1
fi
if [[ -z "$PR_NUMBER" ]]; then
echo "Could not determine PR number from context" >&2
exit 1
fi
printf 'pr_number=%s\n' "$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Extract changelog unreleased entries
id: extract-changelog
shell: bash
env:
CHANGELOG: ${{ inputs.changelog }}
run: |
set -euo pipefail
if [[ ! -f "$CHANGELOG" ]]; then
printf 'unreleased_entries=%s\n' "" >> "$GITHUB_OUTPUT"
exit 0
fi
# Extract everything between [Unreleased] header and the next [X.Y.Z] header
unreleased="$(awk '
/^## \[Unreleased\]/ { in_unreleased=1; next }
/^## \[[0-9]+\.[0-9]+\.[0-9]+\]/ { if (in_unreleased) exit }
in_unreleased && NF { print }
' "$CHANGELOG")"
# Use a temporary file to handle multiline content
tmp_file=$(mktemp)
printf '%s' "$unreleased" > "$tmp_file"
# Read it back and set as output
delimiter="EOF_CHANGELOG"
printf '%s<<%s\n' "unreleased_entries<<$delimiter" "$delimiter" >> "$GITHUB_OUTPUT"
cat "$tmp_file" >> "$GITHUB_OUTPUT"
printf '%s\n' "$delimiter" >> "$GITHUB_OUTPUT"
rm -f "$tmp_file"
- name: Build PR comment markdown
id: build-comment
shell: bash
env:
COMMENT_TITLE: ${{ inputs.comment-title }}
COVERAGE_PCT: ${{ inputs.coverage-percentage }}
BADGE_URL: ${{ inputs.badge-url }}
UNRELEASED: ${{ steps.extract-changelog.outputs.unreleased_entries }}
run: |
set -euo pipefail
# Start building the comment
tmp_file=$(mktemp)
# Add title and coverage section
cat > "$tmp_file" << 'EOF'
<!-- vociferate-pr-review -->
EOF
printf '## %s\n\n' "$COMMENT_TITLE" >> "$tmp_file"
# Coverage badge section
cat >> "$tmp_file" << EOF
### Coverage
![Coverage Badge]($BADGE_URL)
**Coverage:** $COVERAGE_PCT%
EOF
# Changelog section
if [[ -n "$UNRELEASED" ]]; then
cat >> "$tmp_file" << 'EOF'
### Unreleased Changes
EOF
printf '%s\n' "$UNRELEASED" >> "$tmp_file"
printf '\n' >> "$tmp_file"
fi
# Add footer
cat >> "$tmp_file" << 'EOF'
---
*This comment was automatically generated by [vociferate/decorate-pr](https://git.hrafn.xyz/aether/vociferate).*
EOF
# Store as output using delimiter
delimiter="EOF_COMMENT"
printf '%s<<%s\n' "comment_body<<$delimiter" "$delimiter" >> "$GITHUB_OUTPUT"
cat "$tmp_file" >> "$GITHUB_OUTPUT"
printf '%s\n' "$delimiter" >> "$GITHUB_OUTPUT"
rm -f "$tmp_file"
- name: Find and update/post PR comment
id: post-comment
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
PR_NUMBER: ${{ steps.validate.outputs.pr_number }}
COMMENT_BODY: ${{ steps.build-comment.outputs.comment_body }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
API_URL="${SERVER_URL}/api/v1"
if [[ "$SERVER_URL" == *"github.com"* ]]; then
API_URL="https://api.github.com"
fi
# List existing comments to find vociferate comment
comments_url="${API_URL}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
response=$(curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" "$comments_url")
# Find existing vociferate comment by checking for the marker
existing_comment_id=$(printf '%s' "$response" | \
jq -r '.[] | select(.body | contains("vociferate-pr-review")) | .id' 2>/dev/null | \
head -1 || echo "")
if [[ -n "$existing_comment_id" ]]; then
# Update existing comment
update_url="${API_URL}/repos/${REPOSITORY}/issues/comments/${existing_comment_id}"
curl -s -X PATCH \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(printf '{"body": %s}' "$(printf '%s' "$COMMENT_BODY" | jq -Rs .)")" \
"$update_url" > /dev/null
printf 'comment_id=%s\n' "$existing_comment_id" >> "$GITHUB_OUTPUT"
printf 'comment_url=%s/repos/%s/issues/%s#issuecomment-%s\n' \
"$SERVER_URL" "$REPOSITORY" "$PR_NUMBER" "$existing_comment_id" >> "$GITHUB_OUTPUT"
else
# Create new comment
create_url="${API_URL}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
response=$(curl -s -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(printf '{"body": %s}' "$(printf '%s' "$COMMENT_BODY" | jq -Rs .)")" \
"$create_url")
comment_id=$(printf '%s' "$response" | jq -r '.id' 2>/dev/null)
printf 'comment_id=%s\n' "$comment_id" >> "$GITHUB_OUTPUT"
printf 'comment_url=%s/repos/%s/issues/%s#issuecomment-%s\n' \
"$SERVER_URL" "$REPOSITORY" "$PR_NUMBER" "$comment_id" >> "$GITHUB_OUTPUT"
fi