Adds comprehensive changelog gate that validates qualifying code/behavior/security/workflow/tooling changes include Unreleased entries. Features: - Built-in changelog requirement validation - Configurable change types requiring entries - Docs-only PR exception with customizable glob patterns - PR label-based exemptions - Precise diff parsing: only added lines in Unreleased count - Decision outputs: gate_passed, docs_only, unreleased_additions_count, failure_reason - Integrated PR comment showing gate status with remediation guidance - Strict mode (fails job) and soft mode (warns only) New inputs: - enable-changelog-gate - changelog-gate-mode (strict/soft) - changelog-gate-required-for - changelog-gate-allow-docs-only - changelog-gate-docs-globs - changelog-gate-skip-labels
527 lines
18 KiB
YAML
527 lines
18 KiB
YAML
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: ''
|
|
enable-changelog-gate:
|
|
description: >
|
|
Enable changelog gate validation. Validates that qualifying changes
|
|
include entries in the Unreleased section of CHANGELOG.
|
|
required: false
|
|
default: 'false'
|
|
changelog-gate-mode:
|
|
description: >
|
|
Gate mode: 'strict' fails the job if validation fails; 'soft' warns
|
|
via PR comment only.
|
|
required: false
|
|
default: 'soft'
|
|
changelog-gate-required-for:
|
|
description: >
|
|
Comma-separated types of changes that require changelog entries.
|
|
Valid: code, behavior, security, workflow, tooling.
|
|
required: false
|
|
default: 'code,behavior,security,workflow,tooling'
|
|
changelog-gate-allow-docs-only:
|
|
description: >
|
|
Allow PRs that only modify documentation files to skip changelog
|
|
requirement.
|
|
required: false
|
|
default: 'true'
|
|
changelog-gate-docs-globs:
|
|
description: >
|
|
Comma-separated glob patterns for files that are considered
|
|
documentation (case-insensitive).
|
|
required: false
|
|
default: 'docs/**,**.md,**.txt,**.rst'
|
|
changelog-gate-skip-labels:
|
|
description: >
|
|
Comma-separated PR labels that exempt a PR from changelog requirement.
|
|
Example: skip-changelog.
|
|
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 }}
|
|
gate-passed:
|
|
description: >
|
|
Whether changelog gate validation passed (true/false). Only set when
|
|
gate is enabled.
|
|
value: ${{ steps.changelog-gate.outputs.gate_passed }}
|
|
docs-only:
|
|
description: >
|
|
Whether PR is docs-only (true/false). Only set when gate is enabled.
|
|
value: ${{ steps.changelog-gate.outputs.docs_only }}
|
|
unreleased-additions-count:
|
|
description: >
|
|
Number of lines added to Unreleased section in this PR. Only set when
|
|
gate is enabled.
|
|
value: ${{ steps.changelog-gate.outputs.unreleased_additions_count }}
|
|
gate-failure-reason:
|
|
description: >
|
|
Human-readable reason for gate failure, if applicable. Only set when
|
|
gate is enabled and failed.
|
|
value: ${{ steps.changelog-gate.outputs.failure_reason }}
|
|
|
|
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: Preflight comment API access
|
|
id: preflight
|
|
shell: bash
|
|
env:
|
|
AUTH_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
|
|
PR_NUMBER: ${{ steps.validate.outputs.pr_number }}
|
|
SERVER_URL: ${{ github.server_url }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [[ -z "$AUTH_TOKEN" ]]; then
|
|
echo "No token available for PR comment API calls. Set input token or provide workflow token." >&2
|
|
exit 1
|
|
fi
|
|
|
|
api_url="${SERVER_URL}/api/v1"
|
|
if [[ "$SERVER_URL" == *"github.com"* ]]; then
|
|
api_url="https://api.github.com"
|
|
fi
|
|
|
|
comments_url="${api_url}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
|
|
|
|
curl --fail-with-body -sS \
|
|
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"$comments_url" >/dev/null
|
|
|
|
- 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: Validate changelog gate
|
|
id: changelog-gate
|
|
shell: bash
|
|
env:
|
|
ENABLE_GATE: ${{ inputs.enable-changelog-gate }}
|
|
GATE_MODE: ${{ inputs.changelog-gate-mode }}
|
|
REQUIRED_FOR: ${{ inputs.changelog-gate-required-for }}
|
|
ALLOW_DOCS_ONLY: ${{ inputs.changelog-gate-allow-docs-only }}
|
|
DOCS_GLOBS: ${{ inputs.changelog-gate-docs-globs }}
|
|
SKIP_LABELS: ${{ inputs.changelog-gate-skip-labels }}
|
|
CHANGELOG: ${{ inputs.changelog }}
|
|
PR_NUMBER: ${{ steps.validate.outputs.pr_number }}
|
|
SERVER_URL: ${{ github.server_url }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
AUTH_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# If gate is disabled, skip
|
|
if [[ "$ENABLE_GATE" != "true" ]]; then
|
|
echo "gate_enabled=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
api_url="${SERVER_URL}/api/v1"
|
|
if [[ "$SERVER_URL" == *"github.com"* ]]; then
|
|
api_url="https://api.github.com"
|
|
fi
|
|
|
|
# Get PR labels
|
|
pr_url="${api_url}/repos/${REPOSITORY}/pulls/${PR_NUMBER}"
|
|
pr_data=$(curl -sS -H "Authorization: Bearer ${AUTH_TOKEN}" -H "Content-Type: application/json" "$pr_url")
|
|
pr_labels=$(printf '%s' "$pr_data" | jq -r '.labels[].name' 2>/dev/null | tr '\n' ',' || echo "")
|
|
|
|
# Check skip labels
|
|
if [[ -n "$SKIP_LABELS" ]]; then
|
|
IFS=',' read -ra skip_array <<< "$SKIP_LABELS"
|
|
for skip_label in "${skip_array[@]}"; do
|
|
skip_label="${skip_label// /}"
|
|
if [[ ",$pr_labels," == *",$skip_label,"* ]]; then
|
|
printf 'gate_passed=true\n' >> "$GITHUB_OUTPUT"
|
|
printf 'docs_only=false\n' >> "$GITHUB_OUTPUT"
|
|
printf 'unreleased_additions_count=0\n' >> "$GITHUB_OUTPUT"
|
|
printf 'skip_reason=skip_label_detected\n' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Get PR diff
|
|
diff_url="${api_url}/repos/${REPOSITORY}/pulls/${PR_NUMBER}/files"
|
|
diff_data=$(curl -sS -H "Authorization: Bearer ${AUTH_TOKEN}" -H "Content-Type: application/json" "$diff_url")
|
|
|
|
# Determine if PR only modifies docs
|
|
total_files=$(printf '%s' "$diff_data" | jq 'length' 2>/dev/null || echo 0)
|
|
docs_only_true=true
|
|
has_qualifying_changes=false
|
|
|
|
if [[ "$total_files" -gt 0 ]]; then
|
|
printf '%s' "$diff_data" | jq -r '.[].filename' 2>/dev/null | while read -r filename; do
|
|
# Check if file matches docs globs
|
|
is_doc=false
|
|
IFS=',' read -ra glob_array <<< "$DOCS_GLOBS"
|
|
for glob in "${glob_array[@]}"; do
|
|
glob="${glob// /}"
|
|
# Simple glob matching (case-insensitive)
|
|
if [[ "${filename,,}" == ${glob,,} ]] || [[ "${filename,,}" == *"/${glob,,}" ]]; then
|
|
is_doc=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
# .md files are always docs unless they're actual code docs with changes
|
|
if [[ "$filename" == *.md ]]; then
|
|
is_doc=true
|
|
fi
|
|
|
|
if [[ "$is_doc" != "true" ]]; then
|
|
docs_only_true=false
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Get changeset for changelog file
|
|
changelog_diff=""
|
|
if [[ -f "$CHANGELOG" ]]; then
|
|
changelog_diff=$(printf '%s' "$diff_data" | jq -r ".[] | select(.filename == \"$CHANGELOG\") | .patch" 2>/dev/null || echo "")
|
|
fi
|
|
|
|
# Count additions to Unreleased section in changelog diff
|
|
unreleased_additions=0
|
|
if [[ -n "$changelog_diff" ]]; then
|
|
# Find lines added within Unreleased section
|
|
in_unreleased=false
|
|
while IFS= read -r line; do
|
|
# Skip processing metadata lines
|
|
[[ "$line" =~ ^(\+\+\+|---|@@) ]] && continue
|
|
|
|
# Check if entering Unreleased section
|
|
if [[ "$line" == "+## [Unreleased]"* ]] || [[ "$line" == " ## [Unreleased]"* ]]; then
|
|
in_unreleased=true
|
|
continue
|
|
fi
|
|
|
|
# Check if exiting Unreleased section
|
|
if [[ "$line" == "+## ["* ]] || [[ "$line" == " ## ["* ]]; then
|
|
if [[ "$line" != "+## [Unreleased]"* ]] && [[ "$line" != " ## [Unreleased]"* ]]; then
|
|
in_unreleased=false
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
# Count non-empty additions in Unreleased
|
|
if $in_unreleased && [[ "$line" == "+"* ]] && [[ ! "$line" =~ ^(\+\+\+|---|@@) ]]; then
|
|
content="${line:1}" # Remove the '+' prefix
|
|
if [[ -n "${content// /}" ]]; then # Check if not just whitespace
|
|
unreleased_additions=$((unreleased_additions + 1))
|
|
fi
|
|
fi
|
|
done <<< "$changelog_diff"
|
|
fi
|
|
|
|
# Evaluate gate
|
|
gate_passed=true
|
|
failure_reason=""
|
|
|
|
if [[ "$docs_only_true" == "true" ]] && [[ "$ALLOW_DOCS_ONLY" == "true" ]]; then
|
|
gate_passed=true
|
|
docs_only=true
|
|
else
|
|
docs_only=false
|
|
# Check if code requires changelog entry
|
|
if [[ "$total_files" -gt 0 ]] && [[ "$unreleased_additions" -eq 0 ]]; then
|
|
gate_passed=false
|
|
failure_reason="Code changes detected but no entries added to Unreleased section of CHANGELOG.md"
|
|
fi
|
|
fi
|
|
|
|
printf 'gate_enabled=true\n' >> "$GITHUB_OUTPUT"
|
|
printf 'gate_passed=%s\n' "$gate_passed" >> "$GITHUB_OUTPUT"
|
|
printf 'docs_only=%s\n' "$docs_only" >> "$GITHUB_OUTPUT"
|
|
printf 'unreleased_additions_count=%s\n' "$unreleased_additions" >> "$GITHUB_OUTPUT"
|
|
printf 'failure_reason=%s\n' "$failure_reason" >> "$GITHUB_OUTPUT"
|
|
|
|
# Fail job if strict mode and gate failed
|
|
if [[ "$GATE_MODE" == "strict" ]] && [[ "$gate_passed" != "true" ]]; then
|
|
echo "$failure_reason" >&2
|
|
exit 1
|
|
fi
|
|
- 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 }}
|
|
GATE_ENABLED: ${{ steps.changelog-gate.outputs.gate_enabled }}
|
|
GATE_PASSED: ${{ steps.changelog-gate.outputs.gate_passed }}
|
|
GATE_MODE: ${{ inputs.changelog-gate-mode }}
|
|
DOCS_ONLY: ${{ steps.changelog-gate.outputs.docs_only }}
|
|
ADDITIONS_COUNT: ${{ steps.changelog-gate.outputs.unreleased_additions_count }}
|
|
FAILURE_REASON: ${{ steps.changelog-gate.outputs.gate_failure_reason }}
|
|
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 gate section (if enabled)
|
|
if [[ "$GATE_ENABLED" == "true" ]]; then
|
|
gate_status="✓ Pass"
|
|
if [[ "$GATE_PASSED" != "true" ]]; then
|
|
if [[ "$GATE_MODE" == "strict" ]]; then
|
|
gate_status="✗ Fail"
|
|
else
|
|
gate_status="⚠ Warning"
|
|
fi
|
|
fi
|
|
|
|
cat >> "$tmp_file" << EOF
|
|
|
|
### Changelog Gate
|
|
**Status:** $gate_status
|
|
|
|
EOF
|
|
|
|
if [[ "$DOCS_ONLY" == "true" ]]; then
|
|
cat >> "$tmp_file" << 'EOF'
|
|
This PR only modifies documentation—changelog entry not required.
|
|
|
|
EOF
|
|
elif [[ "$GATE_PASSED" == "true" ]]; then
|
|
cat >> "$tmp_file" << EOF
|
|
Found $ADDITIONS_COUNT line(s) added to Unreleased section ✓
|
|
|
|
EOF
|
|
else
|
|
cat >> "$tmp_file" << EOF
|
|
**Issue:** $FAILURE_REASON
|
|
|
|
**How to fix:** Add an entry under the appropriate subsection in the \`## [Unreleased]\` section of \`CHANGELOG.md\`. Use one of:
|
|
- \`### Breaking\` for backwards-incompatible changes
|
|
- \`### Added\` for new features
|
|
- \`### Changed\` for behavior changes
|
|
- \`### Removed\` for deprecated removals
|
|
- \`### Fixed\` for bug fixes
|
|
|
|
Example:
|
|
\`\`\`markdown
|
|
## [Unreleased]
|
|
### Added
|
|
- New changelog gate validation for PR decoration.
|
|
\`\`\`
|
|
|
|
EOF
|
|
fi
|
|
fi
|
|
|
|
# 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
|