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

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