Repository navigation
PRRTE/PMIx: advance shas #1791
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Slash-command handler for /backport. | |
| # | |
| # Posting a comment on a merged PR with: | |
| # | |
| # /backport v5.0.x v4.1.x | |
| # | |
| # is equivalent to manually triggering the "Backport" workflow from the | |
| # GitHub Actions UI with those branch names. Multiple branches may be | |
| # supplied as space- or comma-separated values on the same line. | |
| # | |
| # Only repository owners, organisation members, and collaborators may trigger | |
| # the command. If an unauthorised user attempts /backport, the bot replies | |
| # with an explanatory comment. For valid commands it acknowledges with a 👀 | |
| # reaction; invalid or unrecognised commands get a usage hint as a comment. | |
| name: Backport slash command | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: {} | |
| jobs: | |
| dispatch: | |
| name: Handle /backport comment | |
| runs-on: ubuntu-latest | |
| # Only act on PR comments (issue_comment fires for both issues and PRs). | |
| if: github.event.issue.pull_request != null | |
| permissions: | |
| actions: write # trigger workflow_dispatch | |
| issues: write # post comments | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Parse command and validate PR | |
| id: parse | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const body = context.payload.comment.body; | |
| const commentId = context.payload.comment.id; | |
| const issueNumber = context.payload.issue.number; | |
| const login = context.payload.comment.user.login; | |
| // Best-effort comment helper — if Issues are disabled on | |
| // the repo (common for forks) the call returns 403 and we | |
| // log a warning rather than aborting the workflow. | |
| async function tryComment(text) { | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: text, | |
| }); | |
| } catch (err) { | |
| core.warning(`Could not post comment: ${err.message}`); | |
| } | |
| } | |
| // Detect a bare /backport with no arguments and reply helpfully. | |
| const bareMatch = /^\/backport\s*$/m.test(body); | |
| // Look for /backport with arguments at the start of any line. | |
| const match = body.match(/^\/backport\s+([^\r\n]+)/m); | |
| // If the comment doesn't contain any /backport command at all, | |
| // do nothing — no need to check permissions. | |
| if (!bareMatch && !match) { | |
| core.setOutput('triggered', 'false'); | |
| return; | |
| } | |
| // Use author_association from the webhook payload — no extra | |
| // API call required. getCollaboratorPermissionLevel requires | |
| // org-level "Members" read permission, which is not available | |
| // to either GITHUB_TOKEN or a GitHub App without explicit | |
| // org-level permission grants. | |
| const assoc = context.payload.comment.author_association; | |
| if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc)) { | |
| await tryComment(`⚠️ @${login} Backports can only be triggered by repository owners, organization members, or collaborators.`); | |
| core.setOutput('triggered', 'false'); | |
| return; | |
| } | |
| if (bareMatch && !match) { | |
| core.setOutput('triggered', 'false'); | |
| await tryComment('⚠️ `/backport` requires at least one target branch, e.g. `/backport v5.0.x`.'); | |
| return; | |
| } | |
| if (!match) { | |
| core.setOutput('triggered', 'false'); | |
| return; | |
| } | |
| // Parse branch list (space- or comma-separated). | |
| const branches = match[1].trim().split(/[\s,]+/).filter(Boolean); | |
| if (branches.length === 0) { | |
| // e.g. "/backport ,,," — separators only, no real branch names | |
| core.setOutput('triggered', 'false'); | |
| await tryComment('⚠️ `/backport` requires at least one target branch, e.g. `/backport v5.0.x`.'); | |
| return; | |
| } | |
| // Validate branch names with the same allow-list used in | |
| // backport.yaml so the user gets immediate feedback rather | |
| // than a silent dispatch failure. | |
| const validBranchRe = /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/; | |
| const invalidBranches = branches.filter(b => !validBranchRe.test(b)); | |
| if (invalidBranches.length > 0) { | |
| core.setOutput('triggered', 'false'); | |
| await tryComment(`⚠️ Invalid branch name(s): ${invalidBranches.map(b => `\`${b}\``).join(', ')}. Branch names may only contain alphanumeric characters, dots, hyphens, underscores, and slashes.`); | |
| return; | |
| } | |
| // Confirm the PR is actually merged. | |
| // merged_at is present in the issue_comment webhook payload | |
| // for PRs, so no extra API call is needed. | |
| if (!context.payload.issue.pull_request.merged_at) { | |
| await tryComment('⚠️ Cannot backport: this PR has not been merged yet.'); | |
| core.setOutput('triggered', 'false'); | |
| return; | |
| } | |
| // Acknowledge the command with a 👀 reaction on the triggering comment. | |
| // Ignore 422 (reaction already exists) so re-runs don't fail. | |
| try { | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: commentId, | |
| content: 'eyes', | |
| }); | |
| } catch (err) { | |
| if (err.status !== 422) throw err; | |
| } | |
| core.setOutput('triggered', 'true'); | |
| core.setOutput('pr_number', String(issueNumber)); | |
| core.setOutput('branches', branches.join(',')); | |
| core.notice(`Dispatching backport of PR #${issueNumber} to: ${branches.join(', ')}`); | |
| - name: Trigger backport workflow | |
| if: steps.parse.outputs.triggered == 'true' | |
| uses: actions/github-script@v8 | |
| env: | |
| PR_NUMBER: ${{ steps.parse.outputs.pr_number }} | |
| BRANCHES: ${{ steps.parse.outputs.branches }} | |
| with: | |
| script: | | |
| // workflow_dispatch requires a ref; use the default branch. | |
| const { data: repo } = await github.rest.repos.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'backport.yaml', | |
| ref: repo.default_branch, | |
| inputs: { | |
| pr_number: process.env.PR_NUMBER, | |
| branches: process.env.BRANCHES, | |
| }, | |
| }); |