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/prepare@v1.0.1`
|
||||||
- `git.hrafn.xyz/aether/vociferate/publish@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/coverage-badge@v1.0.1`
|
||||||
|
- `git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.1`
|
||||||
|
|
||||||
## Action Selection Matrix
|
## 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 `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 `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 `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.
|
- Choose root `vociferate` for direct recommend/prepare logic without commit/tag/push behavior.
|
||||||
|
|
||||||
## Preconditions
|
## Preconditions
|
||||||
@@ -139,6 +141,34 @@ jobs:
|
|||||||
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
|
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
|
## Inputs And Outputs Cheatsheet
|
||||||
|
|
||||||
### prepare
|
### prepare
|
||||||
@@ -186,6 +216,24 @@ Primary outputs:
|
|||||||
- `report-url`
|
- `report-url`
|
||||||
- `badge-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
|
## Guardrails For Agents
|
||||||
|
|
||||||
Use these rules to avoid common automation mistakes:
|
Use these rules to avoid common automation mistakes:
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -16,7 +16,7 @@ revision.
|
|||||||
|
|
||||||
## Use In Other Repositories
|
## 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`.
|
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.
|
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 }}"
|
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
|
## Why The Name
|
||||||
|
|
||||||
> **vociferate** _(verb)_: to cry out loudly or forcefully.
|
> **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