From c34d5793da524a7171e614031aeca88acfff4a49 Mon Sep 17 00:00:00 2001 From: GnP Date: Wed, 30 Apr 2025 08:03:43 -0300 Subject: [PATCH 1/2] Add pr comment trigger action --- pr-comment-trigger/README.md | 81 +++++++++++++ pr-comment-trigger/action.yml | 112 ++++++++++++++++++ .../examples/workflow-with-feedback.yml | 102 ++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 pr-comment-trigger/README.md create mode 100644 pr-comment-trigger/action.yml create mode 100644 pr-comment-trigger/examples/workflow-with-feedback.yml diff --git a/pr-comment-trigger/README.md b/pr-comment-trigger/README.md new file mode 100644 index 0000000..aa9c98e --- /dev/null +++ b/pr-comment-trigger/README.md @@ -0,0 +1,81 @@ +# PR Comment Trigger + +This action simplifies the creation of workflows that trigger on a comment on a PR. + +It checks that the comment matches a specific pattern, optionally extracting data from it, and returns the context of the PR. + +Inputs that it receives: + +- `match`: The regex pattern to match the comment against. By default, it will match any comment. +- `ack`: A thumbs up reaction is added to the comment if it matched. Enabled by default. + +Notice that you'll need to grant the workflow `issues: write` permission for the ack to work. + +Outputs: + +- pr-number: The PR number where the triggering comment was added +- pr-head-sha: The SHA of the HEAD commit of the PR +- pr-head-ref: The PR branch +- pr-base-ref: The base branch of the PR +- comment-body: The full comment body +- matches: A JSON object with the regex matches +- match-found: Whether the regex matched or not +- comment-id: The ID of the triggering comment + +Notice that you will also have a lot of information available in the context for `issue_comment`: + +- `github.event.comment`: The [full comment object](https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#get-an-issue-comment) +- `github.event.issue`: The [full issue object](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue) +- [And more](https://docs.github.com/en/webhooks/webhook-events-and-payloads#issue_comment) + +## Usage + +Here's a simple workflow that uses this action: + +```yaml +name: Trigger on a PR comment + +permissions: + issues: write # Required to ack the comment + +on: + issue_comment: + types: [created] + +jobs: + check-comment: + # It's recommended that you use the action in a separate job. + # This makes it easier to skip the rest of the workflow if there was no match. + + # The match on the comment body is not necessary, but it's a good way to skip unrelated comments quickly. + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/foo') }} + + runs-on: ubuntu-latest + + outputs: + match-found: ${{ steps.comment-check.outputs.match-found }} + matches: ${{ steps.comment-check.outputs.matches }} + pr-head-sha: ${{ steps.comment-check.outputs.pr-head-sha }} + pr-head-ref: ${{ steps.comment-check.outputs.pr-head-ref }} + pr-base-ref: ${{ steps.comment-check.outputs.pr-base-ref }} + steps: + - id: comment-check + uses: ensuro/github-actions/pr-comment-trigger@pr-comment-trigger # Use a tag or commit hash here + with: + match: "^/foo (?.*?) (?.*?)$" + + process-command: + needs: check-comment + if: ${{ needs.check-comment.outputs.match-found == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Process Command + run: | + echo "Processing /check on PR #${{ github.event.issue.number }} ${{ needs.check-comment.outputs.pr-head-ref }} -> ${{ needs.check-comment.outputs.pr-base-ref }}" >> $GITHUB_STEP_SUMMARY + echo "bar: ${{ fromJson(needs.check-comment.outputs.matches).bar }}" >> $GITHUB_STEP_SUMMARY + echo "baz: ${{ fromJson(needs.check-comment.outputs.matches).baz }}" >> $GITHUB_STEP_SUMMARY +``` + +Notice that this job will be associated with the main branch and not with the PR. If you want to provide feedback on the PR besides the reaction, you can use checks and/or comments on the PR. + +A more comprehensive workflow example showcasing this is provided in the [examples](./examples) directory. diff --git a/pr-comment-trigger/action.yml b/pr-comment-trigger/action.yml new file mode 100644 index 0000000..ea3d38a --- /dev/null +++ b/pr-comment-trigger/action.yml @@ -0,0 +1,112 @@ +name: "PR Comment Trigger" +description: "Processes PR comments and extracts information based on regex patterns" + +inputs: + match: + description: "Regex pattern to match against the comment" + required: false + default: "" + ack: + description: "Whether to acknowledge the comment with a reaction. Requires permission to write on issues." + required: false + default: "true" + +outputs: + pr-number: + description: "PR number" + value: ${{ fromJson(steps.process-comment.outputs.result).prNumber }} + pr-head-sha: + description: "HEAD SHA of the PR" + value: ${{ fromJson(steps.process-comment.outputs.result).prHeadSha }} + pr-head-ref: + description: "HEAD branch reference of the PR" + value: ${{ fromJson(steps.process-comment.outputs.result).prHeadRef }} + pr-base-ref: + description: "Base branch reference of the PR (usually main)" + value: ${{ fromJson(steps.process-comment.outputs.result).prBaseRef }} + comment-body: + description: "Full comment body" + value: ${{ fromJson(steps.process-comment.outputs.result).commentBody }} + matches: + description: "JSON object with regex matches" + value: ${{ toJson(fromJson(steps.process-comment.outputs.result).matches) }} + match-found: + description: "Whether the regex matched or not" + value: ${{ fromJson(steps.process-comment.outputs.result).matchFound }} + comment-id: + description: "The ID of the triggering comment" + value: ${{ fromJson(steps.process-comment.outputs.result).commentId }} + +runs: + using: "composite" + + steps: + - name: Process PR Comment + id: process-comment + uses: actions/github-script@v7 + with: + script: | + const commentBody = context.payload.comment.body; + const issueNumber = context.payload.issue.number; + const commentId = context.payload.comment.id; + + // Get PR details + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: issueNumber + }); + + const result = { + prNumber: issueNumber, + prHeadSha: pr.head.sha, + prHeadRef: pr.head.ref, + prBaseRef: pr.base.ref, + commentBody: commentBody, + commentId: commentId, + matches: {}, + matchFound: true + }; + + // Process regex match if pattern provided + const matchPattern = '${{ inputs.match }}'; + if (matchPattern) { + try { + const regex = new RegExp(matchPattern, 'm'); + const match = regex.exec(commentBody); + + if (match) { + result.matchFound = true; + + // Add numbered groups + for (let i = 1; i < match.length; i++) { + result.matches[i-1] = match[i] || ""; + } + + // Add named groups if they exist + if (match.groups) { + Object.assign(result.matches, match.groups); + } + + if (${{ inputs.ack }} === 'true') { + // React to the comment with a thumbs up + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + content: '+1', + }); + } + } else { + result.matchFound = false; + } + } catch (error) { + console.log(`Error processing regex: ${error.message}`); + } + } + + return result; + - name: Debug + shell: bash + run: | + echo ${{ steps.process-comment.outputs.result }} diff --git a/pr-comment-trigger/examples/workflow-with-feedback.yml b/pr-comment-trigger/examples/workflow-with-feedback.yml new file mode 100644 index 0000000..074258f --- /dev/null +++ b/pr-comment-trigger/examples/workflow-with-feedback.yml @@ -0,0 +1,102 @@ +name: Trigger on a PR comment + +permissions: + pull-requests: write # Required to add a comment + checks: write # Required to add a check + issues: write # Required to react to the triggering comment + +on: + issue_comment: + types: [created] + +jobs: + check-comment: + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/foo') }} + runs-on: ubuntu-latest + outputs: + match-found: ${{ steps.comment-check.outputs.match-found }} + matches: ${{ steps.comment-check.outputs.matches }} + pr-head-sha: ${{ steps.comment-check.outputs.pr-head-sha }} + pr-head-ref: ${{ steps.comment-check.outputs.pr-head-ref }} + pr-base-ref: ${{ steps.comment-check.outputs.pr-base-ref }} + steps: + - id: comment-check + name: Check PR Comment + uses: ensuro/github-actions/pr-comment-trigger@pr-comment-trigger # Use a tag or commit hash here + with: + match: "^/foo (?.*?) (?.*?)$" + + - name: Debug Outputs + run: | + echo "Match found: ${{ steps.comment-check.outputs.match-found }}" + echo "Matches: ${{ steps.comment-check.outputs.matches }}" + echo "PR head SHA: ${{ steps.comment-check.outputs.pr-head-sha }}" + + process-command: + needs: check-comment + if: ${{ needs.check-comment.outputs.match-found == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Create a check + id: create-check + uses: actions/github-script@v7 + with: + script: | + const { data: check } = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Command: Check Transaction', + head_sha: '${{ needs.check-comment.outputs.pr-head-sha }}', + status: 'in_progress', + output: { + title: 'Transaction Check Initiated', + summary: 'Processing the check transaction command...' + } + }); + return check.id; + + - name: Process Command + run: | + echo "Processing PR #${{ github.event.issue.number }} ${{ needs.check-comment.outputs.pr-head-ref }} -> ${{ needs.check-comment.outputs.pr-base-ref }}" + echo "Bar: ${{ fromJson(needs.check-comment.outputs.matches).bar }}" + echo "Baz: ${{ fromJson(needs.check-comment.outputs.matches).baz }}" + + - name: Comment on PR + id: comment + uses: actions/github-script@v7 + with: + script: | + const matches = JSON.parse(`${{ needs.check-comment.outputs.matches }}`); + + const comment = `## Foo command was executed + **Bar**: ${matches.bar || "unknown"} + **Baz**: \`${matches.baz || "unknown"}\` + **Status**: ✅ Success + + *This check was triggered by a [comment](${{ github.event.comment.html_url }})*`; + + const posted = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.issue.number }}, + body: comment + }); + + return posted.url + + - name: Complete the check + uses: actions/github-script@v7 + if: always() # The check should be completed even if the job fails + with: + script: | + await github.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: ${{ steps.create-check.outputs.result }}, + status: 'completed', + conclusion: '${{ job.status }}', + output: { + title:'Transaction Check Finished', + summary: 'See ${{ steps.comment.outputs.result }}' + } + }); From 76f2d88b5f48366cf5eedde7df47f5e2cd94ffd0 Mon Sep 17 00:00:00 2001 From: GnP Date: Wed, 30 Apr 2025 08:08:30 -0300 Subject: [PATCH 2/2] Fix ack argument type --- pr-comment-trigger/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr-comment-trigger/action.yml b/pr-comment-trigger/action.yml index ea3d38a..2912350 100644 --- a/pr-comment-trigger/action.yml +++ b/pr-comment-trigger/action.yml @@ -88,7 +88,7 @@ runs: Object.assign(result.matches, match.groups); } - if (${{ inputs.ack }} === 'true') { + if ('${{ inputs.ack }}'.toLowerCase() === 'true') { // React to the comment with a thumbs up await github.rest.reactions.createForIssueComment({ owner: context.repo.owner,