VUP Bot #545
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
| name: VUP Bot | |
| on: | |
| workflow_run: | |
| workflows: ["PR Build", "Template Check", "PR Title Check"] | |
| types: | |
| - completed | |
| permissions: | |
| contents: read | |
| actions: read | |
| jobs: | |
| report-status: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event.workflow_run.name == 'PR Build' && contains(fromJson('["pull_request", "pull_request_target"]'), github.event.workflow_run.event) && github.event.workflow_run.conclusion != 'skipped' }} | |
| steps: | |
| - name: Generate App Token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.VUP_BOT_APP_ID }} | |
| private-key: ${{ secrets.VUP_BOT_PRIVATE_KEY }} | |
| - name: Checkout VUP (for scripts) | |
| uses: actions/checkout@v7 | |
| - name: Download Build Reports | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: pr-report-* | |
| merge-multiple: true | |
| path: reports | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download PR Metadata | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: pr-meta | |
| path: pr-meta | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Resolve PR Number | |
| id: pr | |
| run: | | |
| if [ -f pr-meta/pr_number.txt ]; then | |
| num=$(cat pr-meta/pr_number.txt | tr -d '[:space:]') | |
| fi | |
| if [ -z "$num" ]; then | |
| echo "No PR number found in artifact." >&2 | |
| exit 1 | |
| fi | |
| echo "number=$num" >> "$GITHUB_OUTPUT" | |
| - name: Analyze Results | |
| id: analyze | |
| run: | | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" | |
| python3 vup/scripts/analyze_pr_report.py \ | |
| --reports-dir reports \ | |
| --run-url "$RUN_URL" \ | |
| --output comment.md | |
| - name: Comment on PR | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const commentBody = fs.readFileSync('comment.md', 'utf8'); | |
| const prNumber = parseInt('${{ steps.pr.outputs.number }}', 10); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: commentBody | |
| }); | |
| - name: Manage Labels | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const prNumber = parseInt('${{ steps.pr.outputs.number }}', 10); | |
| const buildResult = '${{ steps.analyze.outputs.result }}'; | |
| if (buildResult === 'success') { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['build-passed'] | |
| }); | |
| } catch (e) { | |
| core.warning(`Could not add build-passed label: ${e}`); | |
| } | |
| } | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| name: 'ok-to-build' | |
| }); | |
| } catch (e) { | |
| core.info('ok-to-build label not present (already removed or never added)'); | |
| } | |
| report-pr-check: | |
| runs-on: ubuntu-latest | |
| if: ${{ contains(fromJson('["Template Check", "PR Title Check"]'), github.event.workflow_run.name) && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'skipped' }} | |
| steps: | |
| - name: Generate App Token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.VUP_BOT_APP_ID }} | |
| private-key: ${{ secrets.VUP_BOT_PRIVATE_KEY }} | |
| - name: Download PR Check Report | |
| id: download-report | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: pr-check-report | |
| path: pr-check-report | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update PR Check Comment | |
| if: steps.download-report.outcome == 'success' | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const workflowName = context.payload.workflow_run.name; | |
| const marker = workflowName === 'Template Check' | |
| ? '<!-- template-check -->' | |
| : '<!-- pr-title-check -->'; | |
| const prNumber = Number(fs.readFileSync('pr-check-report/pr_number.txt', 'utf8').trim()); | |
| const status = fs.readFileSync('pr-check-report/status.txt', 'utf8').trim(); | |
| const detailsPath = 'pr-check-report/details.txt'; | |
| const details = fs.existsSync(detailsPath) | |
| ? fs.readFileSync(detailsPath, 'utf8').trim() | |
| : ''; | |
| function escapeHtml(value) { | |
| return value | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| } | |
| function escapeInlineCode(value) { | |
| return value.replace(/`/g, '\\`'); | |
| } | |
| function preformatted(value) { | |
| return `<pre>${escapeHtml(value)}</pre>`; | |
| } | |
| let shouldComment = false; | |
| let commentBody = ''; | |
| if (workflowName === 'Template Check') { | |
| if (status === 'failure') { | |
| shouldComment = true; | |
| commentBody = [ | |
| '### :x: Template Validation Failed', | |
| '', | |
| 'Your template has issues that must be fixed before this PR can be built:', | |
| '', | |
| preformatted(details), | |
| '', | |
| 'Fix the errors above, push your changes, and the check will re-run automatically.', | |
| ].join('\n'); | |
| } else if (status === 'warning') { | |
| shouldComment = true; | |
| commentBody = [ | |
| '### :warning: Template Validation Passed (with warnings)', | |
| '', | |
| preformatted(details), | |
| ].join('\n'); | |
| } | |
| } else if (status === 'failure') { | |
| shouldComment = true; | |
| commentBody = [ | |
| ':warning: PR title does not follow conventions.', | |
| '', | |
| '**New package:** `feat: add <pkgname>` or `feat: add <pkgname> <version>`', | |
| '**Update:** `<pkgname>: update to <version>`', | |
| '**Other:** `fix:`, `chore:`, `docs:`, `ci:`, `refactor:`, `test:`', | |
| '', | |
| `Current title: \`${escapeInlineCode(details)}\``, | |
| ].join('\n'); | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const previous = comments.find(comment => | |
| comment.user.type === 'Bot' && comment.body?.includes(marker) | |
| ); | |
| if (!shouldComment || !commentBody) { | |
| if (previous) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: previous.id, | |
| }); | |
| } | |
| return; | |
| } | |
| // Artifact details come from a pull_request workflow. Avoid pings | |
| // and markers being echoed back through the bot comment. | |
| commentBody = commentBody | |
| .replace(/<!--\s*(template-check|pr-title-check)\s*-->/g, '') | |
| .replace(/@/g, '@<!-- -->') | |
| .trim(); | |
| const maxCommentLength = 60000; | |
| if (commentBody.length > maxCommentLength) { | |
| commentBody = `${commentBody.slice(0, maxCommentLength)}\n\n... output truncated.`; | |
| } | |
| const body = `${marker}\n${commentBody}`; | |
| if (previous) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: previous.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| } |