Skip to content

VUP Bot

VUP Bot #545

Workflow file for this run

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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,
});
}