feat: add decorate-pr composite action for pull request review decoration
This commit is contained in:
48
AGENTS.md
48
AGENTS.md
@@ -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:
|
||||
|
||||
26
README.md
26
README.md
@@ -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
206
decorate-pr/action.yml
Normal 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:** $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
|
||||
Reference in New Issue
Block a user