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' 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