From a94b92dfb2b906f03f2b8cc58513eb08f0487c2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 05:15:21 +0000 Subject: [PATCH] chore: publish from staged --- .../external-plugin-approval-command.yml | 553 +---------- .../external-plugin-command-router.yml | 876 ++++++++++++++++++ .../external-plugin-mark-ready-command.yml | 126 --- .../external-plugin-rereview-command.yml | 8 +- .../external-plugin-rerun-intake-command.yml | 216 ----- 5 files changed, 883 insertions(+), 896 deletions(-) create mode 100644 .github/workflows/external-plugin-command-router.yml delete mode 100644 .github/workflows/external-plugin-mark-ready-command.yml delete mode 100644 .github/workflows/external-plugin-rerun-intake-command.yml diff --git a/.github/workflows/external-plugin-approval-command.yml b/.github/workflows/external-plugin-approval-command.yml index 2302d27d..78b411d6 100644 --- a/.github/workflows/external-plugin-approval-command.yml +++ b/.github/workflows/external-plugin-approval-command.yml @@ -1,440 +1,20 @@ name: External Plugin Approval Commands on: - issue_comment: - types: [created] pull_request: types: [closed] concurrency: - group: external-plugin-intake-${{ github.event.issue.number }} + group: external-plugin-approval-pr-${{ github.event.pull_request.number }} cancel-in-progress: false permissions: - contents: write issues: write - pull-requests: write jobs: - handle-command: - runs-on: ubuntu-latest - if: >- - github.event_name == 'issue_comment' && - !github.event.issue.pull_request && - (contains(github.event.comment.body, '/approve') || contains(github.event.comment.body, '/reject')) - steps: - - name: Checkout staged branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: staged - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 22 - cache: npm - - - name: Parse decision command - id: parse - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - with: - script: | - const path = require('path'); - const { pathToFileURL } = require('url'); - - const approval = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-approval.mjs')).href); - const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); - const parsedCommand = approval.parseDecisionCommand(context.payload.comment.body); - - core.setOutput('should-run', 'false'); - if (!parsedCommand) { - core.info('No supported external plugin approval command was found.'); - return; - } - - const permission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: context.payload.comment.user.login - }); - - const hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); - if (!hasWriteAccess) { - core.info(`Ignoring ${parsedCommand.command} because ${context.payload.comment.user.login} does not have write access.`); - return; - } - - const currentIssue = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - const labelNames = new Set((currentIssue.data.labels || []).map((label) => label.name)); - if (!labelNames.has('external-plugin')) { - core.info('Ignoring command because the issue is not an external plugin submission.'); - return; - } - - const evaluation = await intake.evaluateExternalPluginIssue({ - issue: currentIssue.data, - token: process.env.GITHUB_TOKEN - }); - - const fallbackName = evaluation.plugin?.name ?? `issue-${context.issue.number}`; - const canApprove = labelNames.has('ready-for-review') || labelNames.has('approved'); - const canReject = !labelNames.has('approved'); - - if (parsedCommand.command === 'approve' && !canApprove) { - core.info('Ignoring /approve because the issue is not ready for review.'); - return; - } - - if (parsedCommand.command === 'reject' && !canReject) { - core.info('Ignoring /reject because the issue is already approved.'); - return; - } - - const reactionByCommand = { - approve: 'rocket', - reject: '-1' - }; - - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: reactionByCommand[parsedCommand.command] ?? 'eyes' - }); - - core.setOutput('should-run', 'true'); - core.setOutput('command', parsedCommand.command); - core.setOutput('reason', parsedCommand.reason ?? ''); - core.setOutput('validation-valid', evaluation.valid ? 'true' : 'false'); - core.setOutput('validation-errors', JSON.stringify(evaluation.errors)); - core.setOutput('plugin-name', fallbackName); - core.setOutput('plugin-slug', approval.slugifyPluginName(fallbackName)); - core.setOutput('source-repo', evaluation.plugin?.source?.repo ?? ''); - - - name: Comment blocked approval - if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid != 'true' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - env: - VALIDATION_ERRORS: ${{ steps.parse.outputs.validation-errors }} - PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }} - with: - script: | - const marker = ''; - const errors = JSON.parse(process.env.VALIDATION_ERRORS || '[]'); - const body = [ - marker, - '## ⚠️ External plugin approval blocked', - '', - `The current issue form for **${process.env.PLUGIN_NAME}** no longer passes automated intake validation, so \`/approve\` was not applied.`, - '', - '### Required fixes', - '', - ...(errors.length > 0 ? errors.map((error) => `- ${error}`) : ['- Edit the issue details and let intake rerun automatically, or comment `/rerun-intake` to trigger it again on demand.']) - ].join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100 - }); - - const existingComment = comments.find((comment) => - comment.user?.login === 'github-actions[bot]' && - comment.body?.includes(marker) - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body - }); - } - - - name: Install dependencies - if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' - run: npm ci - - - name: Update external plugin catalog and PR - id: approval_pr - if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - result=$(node ./eng/external-plugin-approval.mjs approve "$GITHUB_EVENT_PATH" --file ./plugins/external.json) - { - echo 'result<> "$GITHUB_OUTPUT" - - plugin_name=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.plugin.name);" "$result") - action=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.action);" "$result") - source_repo=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.plugin.source.repo);" "$result") - plugin_slug='${{ steps.parse.outputs.plugin-slug }}' - issue_number='${{ github.event.issue.number }}' - branch="automation/external-plugin-approve-${issue_number}-${plugin_slug}" - - if [ "$action" = "inserted" ]; then - title_action="Add" - summary_action="add" - else - title_action="Update" - summary_action="update" - fi - - npm run build - bash eng/fix-line-endings.sh - - pr_url="" - pr_number="" - if git diff --quiet; then - pr_number=$(gh pr list --head "$branch" --base staged --json number --jq '.[0].number') - if [ -n "$pr_number" ]; then - pr_url=$(gh pr view "$pr_number" --json url --jq '.url') - fi - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "plugin-name=$plugin_name" >> "$GITHUB_OUTPUT" - echo "action=$action" >> "$GITHUB_OUTPUT" - echo "source-repo=$source_repo" >> "$GITHUB_OUTPUT" - echo "pr-url=$pr_url" >> "$GITHUB_OUTPUT" - echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT" - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -B "$branch" - git add -A - git commit -m "${title_action} external plugin ${plugin_name}" - git push --force-with-lease origin "$branch" - - pr_number=$(gh pr list --head "$branch" --base staged --json number --jq '.[0].number') - pr_body=$(cat <> "$GITHUB_OUTPUT" - echo "plugin-name=$plugin_name" >> "$GITHUB_OUTPUT" - echo "action=$action" >> "$GITHUB_OUTPUT" - echo "source-repo=$source_repo" >> "$GITHUB_OUTPUT" - echo "pr-url=$pr_url" >> "$GITHUB_OUTPUT" - echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT" - - - name: Finalize approval - if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - env: - CHANGED: ${{ steps.approval_pr.outputs.changed }} - ACTION: ${{ steps.approval_pr.outputs.action }} - PLUGIN_NAME: ${{ steps.approval_pr.outputs.plugin-name }} - SOURCE_REPO: ${{ steps.approval_pr.outputs.source-repo }} - PR_URL: ${{ steps.approval_pr.outputs.pr-url }} - PR_NUMBER: ${{ steps.approval_pr.outputs.pr-number }} - with: - script: | - const managedLabels = { - 'external-plugin': { - color: 'FEF2C0', - description: 'Public external plugin submission' - }, - 'awaiting-review': { - color: 'FBCA04', - description: 'Submission is waiting for automated intake validation' - }, - 'ready-for-review': { - color: '0E8A16', - description: 'Submission passed intake validation and is ready for maintainer review' - }, - 'requires-submitter-fixes': { - color: 'D93F0B', - description: 'Submission has quality-gate findings that submitter must fix before maintainer review' - }, - 'approved': { - color: '1D76DB', - description: 'Submission was approved by a maintainer' - }, - 'rejected': { - color: 'B60205', - description: 'Submission was rejected or failed intake validation' - } - }; - - async function ensureLabel(name, config) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name, - color: config.color, - description: config.description - }); - } catch (error) { - if (error.status !== 422) { - throw error; - } - } - } - - async function removeLabel(issueNumber, name) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name - }); - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - } - - async function syncIssueLabels(issueNumber, desiredLabels) { - await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))); - - const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100 - }); - - const currentManagedLabels = currentLabels - .map((label) => label.name) - .filter((name) => Object.prototype.hasOwnProperty.call(managedLabels, name)); - - const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name)); - const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name)); - - if (labelsToAdd.length > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: labelsToAdd - }); - } - - for (const name of labelsToRemove) { - await removeLabel(issueNumber, name); - } - } - - const issueNumber = context.issue.number; - const prNumber = Number(process.env.PR_NUMBER || 0); - const marker = ''; - const action = process.env.ACTION === 'updated' ? 'updated' : 'added'; - const prUrl = process.env.PR_URL; - const body = [ - marker, - '## ✅ External plugin approved', - '', - `A maintainer approved **${process.env.PLUGIN_NAME}**, and the submission issue has been closed.`, - '', - `- **Catalog action:** ${action}`, - `- **Source repository:** \`${process.env.SOURCE_REPO}\``, - prUrl - ? `- **PR against \`staged\`:** ${prUrl}` - : '- **PR against `staged`:** No new PR was needed because the approved listing is already present.' - ].join('\n'); - - await syncIssueLabels(issueNumber, new Set(['external-plugin', 'approved'])); - - if (prNumber > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: ['external-plugin', 'awaiting-review'] - }); - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100 - }); - - const existingComment = comments.find((comment) => - comment.user?.login === 'github-actions[bot]' && - comment.body?.includes(marker) - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body - }); - } - - if (context.payload.issue.state !== 'closed') { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - state: 'closed' - }); - } - sync-merged-pr-labels: runs-on: ubuntu-latest if: >- - github.event_name == 'pull_request' && - github.event.action == 'closed' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'external-plugin') steps: @@ -488,134 +68,3 @@ jobs: name: labelName }); } - - - name: Finalize rejection - if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'reject' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - env: - REASON: ${{ steps.parse.outputs.reason }} - PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }} - with: - script: | - const managedLabels = { - 'external-plugin': { - color: 'FEF2C0', - description: 'Public external plugin submission' - }, - 'awaiting-review': { - color: 'FBCA04', - description: 'Submission is waiting for automated intake validation' - }, - 'ready-for-review': { - color: '0E8A16', - description: 'Submission passed intake validation and is ready for maintainer review' - }, - 'requires-submitter-fixes': { - color: 'D93F0B', - description: 'Submission has quality-gate findings that submitter must fix before maintainer review' - }, - 'approved': { - color: '1D76DB', - description: 'Submission was approved by a maintainer' - }, - 'rejected': { - color: 'B60205', - description: 'Submission was rejected or failed intake validation' - } - }; - - async function ensureLabel(name, config) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name, - color: config.color, - description: config.description - }); - } catch (error) { - if (error.status !== 422) { - throw error; - } - } - } - - async function removeLabel(name) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name - }); - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - } - - await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: ['external-plugin', 'rejected'] - }); - - await removeLabel('awaiting-review'); - await removeLabel('ready-for-review'); - await removeLabel('requires-submitter-fixes'); - await removeLabel('approved'); - - const marker = ''; - const reason = process.env.REASON || 'No additional reason was provided.'; - const body = [ - marker, - '## ❌ External plugin rejected', - '', - `A maintainer rejected **${process.env.PLUGIN_NAME}**, and the submission issue has been closed.`, - '', - '### Reason', - '', - reason, - '', - 'If you address the feedback, edit this issue with the updated details and have the issue author or a maintainer comment `/rerun-intake` to re-run automated intake.' - ].join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100 - }); - - const existingComment = comments.find((comment) => - comment.user?.login === 'github-actions[bot]' && - comment.body?.includes(marker) - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body - }); - } - - if (context.payload.issue.state !== 'closed') { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - state: 'closed' - }); - } diff --git a/.github/workflows/external-plugin-command-router.yml b/.github/workflows/external-plugin-command-router.yml new file mode 100644 index 00000000..e616b1c5 --- /dev/null +++ b/.github/workflows/external-plugin-command-router.yml @@ -0,0 +1,876 @@ +name: External Plugin Command Router + +on: + issue_comment: + types: [created] + +concurrency: + group: external-plugin-intake-${{ github.event.issue.number }} + cancel-in-progress: false + +permissions: + contents: read + issues: write + +jobs: + approval-command: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + if: >- + !github.event.issue.pull_request && + (startsWith(github.event.comment.body, '/approve') || startsWith(github.event.comment.body, '/reject')) + steps: + - name: Checkout staged branch + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: staged + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: npm + + - name: Parse decision command + id: parse + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const path = require('path'); + const { pathToFileURL } = require('url'); + + const approval = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-approval.mjs')).href); + const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); + const parsedCommand = approval.parseDecisionCommand(context.payload.comment.body); + + core.setOutput('should-run', 'false'); + if (!parsedCommand) { + core.info('No supported external plugin approval command was found.'); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.comment.user.login + }); + + const hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); + if (!hasWriteAccess) { + core.info(`Ignoring ${parsedCommand.command} because ${context.payload.comment.user.login} does not have write access.`); + return; + } + + const currentIssue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const labelNames = new Set((currentIssue.data.labels || []).map((label) => label.name)); + if (!labelNames.has('external-plugin')) { + core.info('Ignoring command because the issue is not an external plugin submission.'); + return; + } + + const evaluation = await intake.evaluateExternalPluginIssue({ + issue: currentIssue.data, + token: process.env.GITHUB_TOKEN + }); + + const fallbackName = evaluation.plugin?.name ?? `issue-${context.issue.number}`; + const canApprove = labelNames.has('ready-for-review') || labelNames.has('approved'); + const canReject = !labelNames.has('approved'); + + if (parsedCommand.command === 'approve' && !canApprove) { + core.info('Ignoring /approve because the issue is not ready for review.'); + return; + } + + if (parsedCommand.command === 'reject' && !canReject) { + core.info('Ignoring /reject because the issue is already approved.'); + return; + } + + const reactionByCommand = { + approve: 'rocket', + reject: '-1' + }; + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: reactionByCommand[parsedCommand.command] ?? 'eyes' + }); + + core.setOutput('should-run', 'true'); + core.setOutput('command', parsedCommand.command); + core.setOutput('reason', parsedCommand.reason ?? ''); + core.setOutput('validation-valid', evaluation.valid ? 'true' : 'false'); + core.setOutput('validation-errors', JSON.stringify(evaluation.errors)); + core.setOutput('plugin-name', fallbackName); + core.setOutput('plugin-slug', approval.slugifyPluginName(fallbackName)); + core.setOutput('source-repo', evaluation.plugin?.source?.repo ?? ''); + + - name: Comment blocked approval + if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid != 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + VALIDATION_ERRORS: ${{ steps.parse.outputs.validation-errors }} + PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }} + with: + script: | + const marker = ''; + const errors = JSON.parse(process.env.VALIDATION_ERRORS || '[]'); + const body = [ + marker, + '## ⚠️ External plugin approval blocked', + '', + `The current issue form for **${process.env.PLUGIN_NAME}** no longer passes automated intake validation, so \`/approve\` was not applied.`, + '', + '### Required fixes', + '', + ...(errors.length > 0 ? errors.map((error) => `- ${error}`) : ['- Edit the issue details and let intake rerun automatically, or comment `/rerun-intake` to trigger it again on demand.']) + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100 + }); + + const existingComment = comments.find((comment) => + comment.user?.login === 'github-actions[bot]' && + comment.body?.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + + - name: Install dependencies + if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' + run: npm ci + + - name: Update external plugin catalog and PR + id: approval_pr + if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + result=$(node ./eng/external-plugin-approval.mjs approve "$GITHUB_EVENT_PATH" --file ./plugins/external.json) + { + echo 'result<> "$GITHUB_OUTPUT" + + plugin_name=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.plugin.name);" "$result") + action=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.action);" "$result") + source_repo=$(node -e "const data = JSON.parse(process.argv[1]); process.stdout.write(data.plugin.source.repo);" "$result") + plugin_slug='${{ steps.parse.outputs.plugin-slug }}' + issue_number='${{ github.event.issue.number }}' + branch="automation/external-plugin-approve-${issue_number}-${plugin_slug}" + + if [ "$action" = "inserted" ]; then + title_action="Add" + summary_action="add" + else + title_action="Update" + summary_action="update" + fi + + npm run build + bash eng/fix-line-endings.sh + + pr_url="" + pr_number="" + if git diff --quiet; then + pr_number=$(gh pr list --head "$branch" --base staged --json number --jq '.[0].number') + if [ -n "$pr_number" ]; then + pr_url=$(gh pr view "$pr_number" --json url --jq '.url') + fi + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "plugin-name=$plugin_name" >> "$GITHUB_OUTPUT" + echo "action=$action" >> "$GITHUB_OUTPUT" + echo "source-repo=$source_repo" >> "$GITHUB_OUTPUT" + echo "pr-url=$pr_url" >> "$GITHUB_OUTPUT" + echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$branch" + git add -A + git commit -m "${title_action} external plugin ${plugin_name}" + git push --force-with-lease origin "$branch" + + pr_number=$(gh pr list --head "$branch" --base staged --json number --jq '.[0].number') + pr_body=$(cat <> "$GITHUB_OUTPUT" + echo "plugin-name=$plugin_name" >> "$GITHUB_OUTPUT" + echo "action=$action" >> "$GITHUB_OUTPUT" + echo "source-repo=$source_repo" >> "$GITHUB_OUTPUT" + echo "pr-url=$pr_url" >> "$GITHUB_OUTPUT" + echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT" + + - name: Finalize approval + if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'approve' && steps.parse.outputs.validation-valid == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + CHANGED: ${{ steps.approval_pr.outputs.changed }} + ACTION: ${{ steps.approval_pr.outputs.action }} + PLUGIN_NAME: ${{ steps.approval_pr.outputs.plugin-name }} + SOURCE_REPO: ${{ steps.approval_pr.outputs.source-repo }} + PR_URL: ${{ steps.approval_pr.outputs.pr-url }} + PR_NUMBER: ${{ steps.approval_pr.outputs.pr-number }} + with: + script: | + const managedLabels = { + 'external-plugin': { + color: 'FEF2C0', + description: 'Public external plugin submission' + }, + 'awaiting-review': { + color: 'FBCA04', + description: 'Submission is waiting for automated intake validation' + }, + 'ready-for-review': { + color: '0E8A16', + description: 'Submission passed intake validation and is ready for maintainer review' + }, + 'requires-submitter-fixes': { + color: 'D93F0B', + description: 'Submission has quality-gate findings that submitter must fix before maintainer review' + }, + 'approved': { + color: '1D76DB', + description: 'Submission was approved by a maintainer' + }, + 'rejected': { + color: 'B60205', + description: 'Submission was rejected or failed intake validation' + } + }; + + async function ensureLabel(name, config) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name, + color: config.color, + description: config.description + }); + } catch (error) { + if (error.status !== 422) { + throw error; + } + } + } + + async function removeLabel(issueNumber, name) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + } + + async function syncIssueLabels(issueNumber, desiredLabels) { + await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))); + + const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100 + }); + + const currentManagedLabels = currentLabels + .map((label) => label.name) + .filter((name) => Object.prototype.hasOwnProperty.call(managedLabels, name)); + + const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name)); + const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name)); + + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: labelsToAdd + }); + } + + for (const name of labelsToRemove) { + await removeLabel(issueNumber, name); + } + } + + const issueNumber = context.issue.number; + const prNumber = Number(process.env.PR_NUMBER || 0); + const marker = ''; + const action = process.env.ACTION === 'updated' ? 'updated' : 'added'; + const prUrl = process.env.PR_URL; + const body = [ + marker, + '## ✅ External plugin approved', + '', + `A maintainer approved **${process.env.PLUGIN_NAME}**, and the submission issue has been closed.`, + '', + `- **Catalog action:** ${action}`, + `- **Source repository:** \`${process.env.SOURCE_REPO}\``, + prUrl + ? `- **PR against \`staged\`:** ${prUrl}` + : '- **PR against `staged`:** No new PR was needed because the approved listing is already present.' + ].join('\n'); + + await syncIssueLabels(issueNumber, new Set(['external-plugin', 'approved'])); + + if (prNumber > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['external-plugin', 'awaiting-review'] + }); + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100 + }); + + const existingComment = comments.find((comment) => + comment.user?.login === 'github-actions[bot]' && + comment.body?.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body + }); + } + + if (context.payload.issue.state !== 'closed') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed' + }); + } + + - name: Finalize rejection + if: steps.parse.outputs.should-run == 'true' && steps.parse.outputs.command == 'reject' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + REASON: ${{ steps.parse.outputs.reason }} + PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }} + with: + script: | + const managedLabels = { + 'external-plugin': { + color: 'FEF2C0', + description: 'Public external plugin submission' + }, + 'awaiting-review': { + color: 'FBCA04', + description: 'Submission is waiting for automated intake validation' + }, + 'ready-for-review': { + color: '0E8A16', + description: 'Submission passed intake validation and is ready for maintainer review' + }, + 'requires-submitter-fixes': { + color: 'D93F0B', + description: 'Submission has quality-gate findings that submitter must fix before maintainer review' + }, + 'approved': { + color: '1D76DB', + description: 'Submission was approved by a maintainer' + }, + 'rejected': { + color: 'B60205', + description: 'Submission was rejected or failed intake validation' + } + }; + + async function ensureLabel(name, config) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name, + color: config.color, + description: config.description + }); + } catch (error) { + if (error.status !== 422) { + throw error; + } + } + } + + async function removeLabel(name) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + } + + await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['external-plugin', 'rejected'] + }); + + await removeLabel('awaiting-review'); + await removeLabel('ready-for-review'); + await removeLabel('requires-submitter-fixes'); + await removeLabel('approved'); + + const marker = ''; + const reason = process.env.REASON || 'No additional reason was provided.'; + const body = [ + marker, + '## ❌ External plugin rejected', + '', + `A maintainer rejected **${process.env.PLUGIN_NAME}**, and the submission issue has been closed.`, + '', + '### Reason', + '', + reason, + '', + 'If you address the feedback, edit this issue with the updated details and have the issue author or a maintainer comment `/rerun-intake` to re-run automated intake.' + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100 + }); + + const existingComment = comments.find((comment) => + comment.user?.login === 'github-actions[bot]' && + comment.body?.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + + if (context.payload.issue.state !== 'closed') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed' + }); + } + + mark-ready-command: + runs-on: ubuntu-latest + if: >- + !github.event.issue.pull_request && + startsWith(github.event.comment.body, '/mark-ready-for-review') + steps: + - name: Checkout staged branch + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: staged + + - name: Apply explicit ready-for-review override + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const path = require('path'); + const { pathToFileURL } = require('url'); + + const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); + const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); + + const parsed = intake.parseMarkReadyForReviewCommand(context.payload.comment.body); + if (!parsed) { + core.info('No supported /mark-ready-for-review command was found.'); + return; + } + + const actor = context.payload.comment.user?.login; + if (!actor || context.payload.comment.user?.type === 'Bot' || actor === 'github-actions[bot]') { + core.info('Ignoring command from a bot or unknown actor.'); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: actor + }); + const hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); + if (!hasWriteAccess) { + core.info(`Ignoring /mark-ready-for-review because ${actor} does not have write access.`); + return; + } + + const { data: currentIssue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const labelNames = new Set((currentIssue.labels || []).map((label) => label.name)); + if (!labelNames.has('external-plugin')) { + core.info('Ignoring command because issue is not an external plugin submission.'); + return; + } + + if (labelNames.has('approved')) { + core.info('Ignoring command because issue is already approved.'); + return; + } + + if (!labelNames.has('requires-submitter-fixes')) { + core.info('Ignoring command because issue is not currently blocked by submitter-fix gates.'); + return; + } + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + + await intakeState.syncExternalPluginIntakeLabels({ + github, + owner: context.repo.owner, + repo: context.repo.repo, + issueNumber: context.issue.number, + desiredLabels: new Set(['external-plugin', 'ready-for-review']) + }); + + const marker = ''; + const reason = parsed.reason || 'No reason provided.'; + const body = [ + marker, + '## ✅ External plugin manually moved to ready-for-review', + '', + `Maintainer **${actor}** used \`${intake.MARK_READY_FOR_REVIEW_COMMAND}\` to move this submission from \`requires-submitter-fixes\` to \`ready-for-review\`.`, + '', + '### Reason', + '', + reason + ].join('\n'); + + await intakeState.upsertExternalPluginIntakeComment({ + github, + owner: context.repo.owner, + repo: context.repo.repo, + issueNumber: context.issue.number, + marker, + body + }); + + if (currentIssue.state === 'closed') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'open' + }); + } + + rerun-intake-parse: + runs-on: ubuntu-latest + if: >- + !github.event.issue.pull_request && + startsWith(github.event.comment.body, '/rerun-intake') + outputs: + should-run: ${{ steps.evaluate.outputs.should-run }} + base-result: ${{ steps.evaluate.outputs.base-result }} + valid: ${{ steps.evaluate.outputs.valid }} + plugin-json: ${{ steps.evaluate.outputs.plugin-json }} + issue-state: ${{ steps.evaluate.outputs.issue-state }} + issue-labels: ${{ steps.evaluate.outputs.issue-labels }} + steps: + - name: Checkout staged branch + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: staged + + - name: Validate command and evaluate intake + id: evaluate + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + script: | + const path = require('path'); + const { pathToFileURL } = require('url'); + + const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); + + core.setOutput('should-run', 'false'); + + const commentAuthor = context.payload.comment.user?.login; + if (!commentAuthor || context.payload.comment.user?.type === 'Bot' || commentAuthor === 'github-actions[bot]') { + core.info('Ignoring /rerun-intake from a bot or unknown actor.'); + return; + } + + if (!intake.parseRerunIntakeCommand(context.payload.comment.body)) { + core.info('No supported /rerun-intake command was found.'); + return; + } + + const { data: currentIssue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const labelNames = new Set((currentIssue.labels || []).map((label) => label.name)); + const isExternalPluginIssue = + labelNames.has('external-plugin') || + String(currentIssue.body || '').includes(intake.ISSUE_FORM_MARKER); + if (!isExternalPluginIssue) { + core.info('Ignoring /rerun-intake because the issue is not an external plugin submission.'); + return; + } + + if (labelNames.has('approved') || labelNames.has('re-review-due') || labelNames.has('re-review-follow-up')) { + core.info('Ignoring /rerun-intake because the issue is already approved or in the six-month re-review flow.'); + return; + } + + const issueAuthor = currentIssue.user?.login; + const isIssueAuthor = Boolean(issueAuthor && commentAuthor === issueAuthor); + + let hasWriteAccess = false; + if (!isIssueAuthor) { + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commentAuthor + }); + hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); + } + + if (!isIssueAuthor && !hasWriteAccess) { + core.info(`Ignoring /rerun-intake because ${commentAuthor} is neither the issue author nor a maintainer.`); + return; + } + + const canRerunFromCurrentState = currentIssue.state === 'open' || labelNames.has('rejected'); + if (!canRerunFromCurrentState) { + core.info('Ignoring /rerun-intake because the issue is closed outside the intake/rejection flow.'); + return; + } + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes' + }); + + const baseResult = await intake.evaluateExternalPluginIssue({ + issue: currentIssue, + token: process.env.GITHUB_TOKEN, + runId: context.runId, + owner: context.repo.owner, + repo: context.repo.repo + }); + + core.setOutput('should-run', 'true'); + core.setOutput('base-result', JSON.stringify(baseResult)); + core.setOutput('valid', baseResult.valid ? 'true' : 'false'); + core.setOutput('plugin-json', JSON.stringify(baseResult.plugin || {})); + core.setOutput('issue-state', currentIssue.state); + core.setOutput('issue-labels', JSON.stringify([...labelNames])); + + rerun-intake-quality-gates: + needs: rerun-intake-parse + if: >- + needs.rerun-intake-parse.outputs.should-run == 'true' && + needs.rerun-intake-parse.outputs.valid == 'true' + uses: ./.github/workflows/external-plugin-quality-gates.yml + with: + plugin-json: ${{ needs.rerun-intake-parse.outputs.plugin-json }} + + rerun-intake-apply-state: + runs-on: ubuntu-latest + needs: [rerun-intake-parse, rerun-intake-quality-gates] + if: always() && needs.rerun-intake-parse.outputs.should-run == 'true' + steps: + - name: Checkout staged branch + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: staged + + - name: Apply merged intake evaluation + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + BASE_RESULT_JSON: ${{ needs.rerun-intake-parse.outputs.base-result }} + BASE_VALID: ${{ needs.rerun-intake-parse.outputs.valid }} + QUALITY_RESULT_JSON: ${{ needs.rerun-intake-quality-gates.outputs.quality-result }} + QUALITY_JOB_RESULT: ${{ needs.rerun-intake-quality-gates.result }} + ISSUE_STATE: ${{ needs.rerun-intake-parse.outputs.issue-state }} + ISSUE_LABELS: ${{ needs.rerun-intake-parse.outputs.issue-labels }} + with: + script: | + const path = require('path'); + const { pathToFileURL } = require('url'); + + const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); + const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); + + const baseResult = JSON.parse(process.env.BASE_RESULT_JSON); + let finalResult = baseResult; + + if (process.env.BASE_VALID === 'true') { + let qualityResult; + if (process.env.QUALITY_JOB_RESULT === 'failure' || process.env.QUALITY_JOB_RESULT === 'cancelled') { + qualityResult = { + overall_status: 'infra_error', + skill_validator_status: 'infra_error', + smoke_status: 'infra_error', + failure_class: 'infra', + summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', + }; + } else if (process.env.QUALITY_RESULT_JSON) { + qualityResult = JSON.parse(process.env.QUALITY_RESULT_JSON); + } else { + qualityResult = { + overall_status: 'infra_error', + skill_validator_status: 'infra_error', + smoke_status: 'infra_error', + failure_class: 'infra', + summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', + }; + } + + finalResult = intake.applyQualityGateResult(baseResult, qualityResult, context.runId, context.repo.owner, context.repo.repo); + } + + await intakeState.applyExternalPluginIntakeEvaluation({ + github, + owner: context.repo.owner, + repo: context.repo.repo, + issueNumber: context.issue.number, + evaluation: finalResult + }); + + const issueState = process.env.ISSUE_STATE; + const labels = new Set(JSON.parse(process.env.ISSUE_LABELS || '[]')); + if (finalResult.intakeState === 'rejected' && issueState === 'open') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed' + }); + return; + } + + if (finalResult.intakeState !== 'rejected' && issueState === 'closed' && labels.has('rejected')) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'open' + }); + } diff --git a/.github/workflows/external-plugin-mark-ready-command.yml b/.github/workflows/external-plugin-mark-ready-command.yml deleted file mode 100644 index 6cbd3fa3..00000000 --- a/.github/workflows/external-plugin-mark-ready-command.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: External Plugin Mark Ready Command - -on: - issue_comment: - types: [created] - -concurrency: - group: external-plugin-intake-${{ github.event.issue.number }} - cancel-in-progress: false - -permissions: - contents: read - issues: write - -jobs: - mark-ready: - runs-on: ubuntu-latest - if: >- - !github.event.issue.pull_request && - startsWith(github.event.comment.body, '/mark-ready-for-review') - steps: - - name: Checkout staged branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: staged - - - name: Apply explicit ready-for-review override - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - with: - script: | - const path = require('path'); - const { pathToFileURL } = require('url'); - - const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); - const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); - - const parsed = intake.parseMarkReadyForReviewCommand(context.payload.comment.body); - if (!parsed) { - core.info('No supported /mark-ready-for-review command was found.'); - return; - } - - const actor = context.payload.comment.user?.login; - if (!actor || context.payload.comment.user?.type === 'Bot' || actor === 'github-actions[bot]') { - core.info('Ignoring command from a bot or unknown actor.'); - return; - } - - const permission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: actor - }); - const hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); - if (!hasWriteAccess) { - core.info(`Ignoring /mark-ready-for-review because ${actor} does not have write access.`); - return; - } - - const { data: currentIssue } = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - const labelNames = new Set((currentIssue.labels || []).map((label) => label.name)); - if (!labelNames.has('external-plugin')) { - core.info('Ignoring command because issue is not an external plugin submission.'); - return; - } - - if (labelNames.has('approved')) { - core.info('Ignoring command because issue is already approved.'); - return; - } - - if (!labelNames.has('requires-submitter-fixes')) { - core.info('Ignoring command because issue is not currently blocked by submitter-fix gates.'); - return; - } - - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '+1' - }); - - await intakeState.syncExternalPluginIntakeLabels({ - github, - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: context.issue.number, - desiredLabels: new Set(['external-plugin', 'ready-for-review']) - }); - - const marker = ''; - const reason = parsed.reason || 'No reason provided.'; - const body = [ - marker, - '## ✅ External plugin manually moved to ready-for-review', - '', - `Maintainer **${actor}** used \`${intake.MARK_READY_FOR_REVIEW_COMMAND}\` to move this submission from \`requires-submitter-fixes\` to \`ready-for-review\`.`, - '', - '### Reason', - '', - reason - ].join('\n'); - - await intakeState.upsertExternalPluginIntakeComment({ - github, - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: context.issue.number, - marker, - body - }); - - if (currentIssue.state === 'closed') { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - state: 'open' - }); - } diff --git a/.github/workflows/external-plugin-rereview-command.yml b/.github/workflows/external-plugin-rereview-command.yml index de48c29b..e34f6ecb 100644 --- a/.github/workflows/external-plugin-rereview-command.yml +++ b/.github/workflows/external-plugin-rereview-command.yml @@ -1,16 +1,20 @@ -name: External Plugin Re-review Commands +name: External Plugin Re-review Command on: issue_comment: types: [created] +concurrency: + group: external-plugin-rereview-${{ github.event.issue.number }} + cancel-in-progress: false + permissions: contents: write issues: write pull-requests: write jobs: - handle-command: + rereview-command: runs-on: ubuntu-latest if: >- !github.event.issue.pull_request && diff --git a/.github/workflows/external-plugin-rerun-intake-command.yml b/.github/workflows/external-plugin-rerun-intake-command.yml deleted file mode 100644 index 1ae6dc78..00000000 --- a/.github/workflows/external-plugin-rerun-intake-command.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: External Plugin Rerun Intake Commands - -on: - issue_comment: - types: [created] - -concurrency: - group: external-plugin-intake-${{ github.event.issue.number }} - cancel-in-progress: false - -permissions: - contents: read - issues: write - -jobs: - parse-command: - runs-on: ubuntu-latest - if: >- - !github.event.issue.pull_request && - startsWith(github.event.comment.body, '/rerun-intake') - outputs: - should-run: ${{ steps.evaluate.outputs.should-run }} - base-result: ${{ steps.evaluate.outputs.base-result }} - valid: ${{ steps.evaluate.outputs.valid }} - plugin-json: ${{ steps.evaluate.outputs.plugin-json }} - issue-state: ${{ steps.evaluate.outputs.issue-state }} - issue-labels: ${{ steps.evaluate.outputs.issue-labels }} - steps: - - name: Checkout staged branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: staged - - - name: Validate command and evaluate intake - id: evaluate - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - script: | - const path = require('path'); - const { pathToFileURL } = require('url'); - - const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); - - core.setOutput('should-run', 'false'); - - const commentAuthor = context.payload.comment.user?.login; - if (!commentAuthor || context.payload.comment.user?.type === 'Bot' || commentAuthor === 'github-actions[bot]') { - core.info('Ignoring /rerun-intake from a bot or unknown actor.'); - return; - } - - if (!intake.parseRerunIntakeCommand(context.payload.comment.body)) { - core.info('No supported /rerun-intake command was found.'); - return; - } - - const { data: currentIssue } = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - const labelNames = new Set((currentIssue.labels || []).map((label) => label.name)); - const isExternalPluginIssue = - labelNames.has('external-plugin') || - String(currentIssue.body || '').includes(intake.ISSUE_FORM_MARKER); - if (!isExternalPluginIssue) { - core.info('Ignoring /rerun-intake because the issue is not an external plugin submission.'); - return; - } - - if (labelNames.has('approved') || labelNames.has('re-review-due') || labelNames.has('re-review-follow-up')) { - core.info('Ignoring /rerun-intake because the issue is already approved or in the six-month re-review flow.'); - return; - } - - const issueAuthor = currentIssue.user?.login; - const isIssueAuthor = Boolean(issueAuthor && commentAuthor === issueAuthor); - - let hasWriteAccess = false; - if (!isIssueAuthor) { - const permission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: commentAuthor - }); - hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.data.permission); - } - - if (!isIssueAuthor && !hasWriteAccess) { - core.info(`Ignoring /rerun-intake because ${commentAuthor} is neither the issue author nor a maintainer.`); - return; - } - - const canRerunFromCurrentState = currentIssue.state === 'open' || labelNames.has('rejected'); - if (!canRerunFromCurrentState) { - core.info('Ignoring /rerun-intake because the issue is closed outside the intake/rejection flow.'); - return; - } - - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'eyes' - }); - - const baseResult = await intake.evaluateExternalPluginIssue({ - issue: currentIssue, - token: process.env.GITHUB_TOKEN, - runId: context.runId, - owner: context.repo.owner, - repo: context.repo.repo - }); - - core.setOutput('should-run', 'true'); - core.setOutput('base-result', JSON.stringify(baseResult)); - core.setOutput('valid', baseResult.valid ? 'true' : 'false'); - core.setOutput('plugin-json', JSON.stringify(baseResult.plugin || {})); - core.setOutput('issue-state', currentIssue.state); - core.setOutput('issue-labels', JSON.stringify([...labelNames])); - - quality-gates: - needs: parse-command - if: >- - needs.parse-command.outputs.should-run == 'true' && - needs.parse-command.outputs.valid == 'true' - uses: ./.github/workflows/external-plugin-quality-gates.yml - with: - plugin-json: ${{ needs.parse-command.outputs.plugin-json }} - - apply-state: - runs-on: ubuntu-latest - needs: [parse-command, quality-gates] - if: always() && needs.parse-command.outputs.should-run == 'true' - steps: - - name: Checkout staged branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: staged - - - name: Apply merged intake evaluation - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - env: - BASE_RESULT_JSON: ${{ needs.parse-command.outputs.base-result }} - BASE_VALID: ${{ needs.parse-command.outputs.valid }} - QUALITY_RESULT_JSON: ${{ needs.quality-gates.outputs.quality-result }} - QUALITY_JOB_RESULT: ${{ needs.quality-gates.result }} - ISSUE_STATE: ${{ needs.parse-command.outputs.issue-state }} - ISSUE_LABELS: ${{ needs.parse-command.outputs.issue-labels }} - with: - script: | - const path = require('path'); - const { pathToFileURL } = require('url'); - - const intake = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake.mjs')).href); - const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); - - const baseResult = JSON.parse(process.env.BASE_RESULT_JSON); - let finalResult = baseResult; - - if (process.env.BASE_VALID === 'true') { - let qualityResult; - if (process.env.QUALITY_JOB_RESULT === 'failure' || process.env.QUALITY_JOB_RESULT === 'cancelled') { - qualityResult = { - overall_status: 'infra_error', - skill_validator_status: 'infra_error', - smoke_status: 'infra_error', - failure_class: 'infra', - summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', - }; - } else if (process.env.QUALITY_RESULT_JSON) { - qualityResult = JSON.parse(process.env.QUALITY_RESULT_JSON); - } else { - qualityResult = { - overall_status: 'infra_error', - skill_validator_status: 'infra_error', - smoke_status: 'infra_error', - failure_class: 'infra', - summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', - }; - } - - finalResult = intake.applyQualityGateResult(baseResult, qualityResult, context.runId, context.repo.owner, context.repo.repo); - } - - await intakeState.applyExternalPluginIntakeEvaluation({ - github, - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: context.issue.number, - evaluation: finalResult - }); - - const issueState = process.env.ISSUE_STATE; - const labels = new Set(JSON.parse(process.env.ISSUE_LABELS || '[]')); - if (finalResult.intakeState === 'rejected' && issueState === 'open') { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - state: 'closed' - }); - return; - } - - if (finalResult.intakeState !== 'rejected' && issueState === 'closed' && labels.has('rejected')) { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - state: 'open' - }); - }