Skip to content

PR Comment

PR Comment #1089

Workflow file for this run

name: PR Comment
on:
workflow_run:
workflows: ["PR Check"]
types: [completed]
pull_request_review:
types: [submitted]
pull_request_target:
types: [opened, edited, reopened]
permissions:
actions: read
checks: read
contents: read
pull-requests: write
jobs:
comment:
name: Post PR comment
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request'
steps:
- uses: actions/checkout@v5
- name: Download PR metadata
id: download
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: pr-meta
path: pr-meta
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve PR context
id: context
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
// Determine the PR number
let prNumber;
const numberFile = 'pr-meta/number.txt';
if (fs.existsSync(numberFile)) {
prNumber = parseInt(fs.readFileSync(numberFile, 'utf8').trim());
}
if (!prNumber) {
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
prNumber = prs[0].number;
} else {
const headSha = context.payload.workflow_run.head_sha;
const { data: prList } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 100,
});
const match = prList.find(pr => pr.head.sha === headSha);
if (!match) {
console.log(`No open PR found for SHA ${headSha} — skipping.`);
return;
}
prNumber = match.number;
}
}
// Fetch existing bot comment (if any) and write to file for Python
const marker = '<!-- pr-check-bot -->';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
fs.mkdirSync('pr-meta', { recursive: true });
fs.writeFileSync('pr-meta/existing-comment.md', existing.body);
fs.writeFileSync('pr-meta/existing-comment-id.txt', String(existing.id));
}
core.setOutput('pr_number', prNumber);
- name: Prepare comment
if: steps.context.outputs.pr_number
env:
CHECK_RUN_ID: ${{ github.event.workflow_run.id }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: python lib/checks/prepare-comment.py
- name: Post or update comment
if: steps.context.outputs.pr_number
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const actionFile = 'pr-meta/action.txt';
if (!fs.existsSync(actionFile)) return;
const action = fs.readFileSync(actionFile, 'utf8').trim();
if (action === 'skip') {
console.log('Nothing to do — skipping.');
return;
}
const bodyFile = 'pr-meta/final-comment.md';
if (!fs.existsSync(bodyFile)) {
console.log('No comment body found — skipping.');
return;
}
const body = fs.readFileSync(bodyFile, 'utf8').trim();
const prNumber = parseInt('${{ steps.context.outputs.pr_number }}');
if (action === 'create') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
console.log('Created bot comment.');
} else if (action === 'update') {
const commentId = parseInt(fs.readFileSync('pr-meta/existing-comment-id.txt', 'utf8').trim());
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
body,
});
console.log('Updated bot comment.');
}
// Auto-assign Lissy93 for review when appropriate (non-critical)
try {
const readInt = (f) => fs.existsSync(f) ? parseInt(fs.readFileSync(f, 'utf8').trim()) : NaN;
const errors = readInt('pr-meta/error-count.txt');
const total = readInt('pr-meta/findings-count.txt');
const shouldAssign =
(action === 'create' && errors === 0 && total === 0) ||
(action === 'update' && errors === 0);
if (shouldAssign) {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
assignees: ['Lissy93'],
});
console.log('Assigned Lissy93 for review.');
}
} catch (err) {
console.log(`Failed to auto-assign: ${err.message}`);
}
review-notify:
name: Notify maintainer
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_review'
steps:
- uses: actions/checkout@v5
- name: Fetch review and CI context
id: context
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
// Fetch reviews — extract user login and state
const { data: reviews } = await github.rest.pulls.listReviews({
owner, repo, pull_number: pr.number, per_page: 100,
});
const reviewData = reviews.map(r => ({
user: r.user.login,
state: r.state,
}));
// Fetch check runs for the PR head SHA
const { data: { check_runs } } = await github.rest.checks.listForRef({
owner, repo, ref: pr.head.sha, per_page: 100,
});
const checkData = check_runs.map(cr => ({
status: cr.status,
conclusion: cr.conclusion,
}));
// Check if we already posted the notification
const marker = '<!-- pr-review-ready -->';
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: pr.number, per_page: 100,
});
const alreadyNotified = comments.some(c => c.body.includes(marker));
// Write data for Python
fs.mkdirSync('pr-meta', { recursive: true });
fs.writeFileSync('pr-meta/reviews.json', JSON.stringify(reviewData));
fs.writeFileSync('pr-meta/check-runs.json', JSON.stringify(checkData));
fs.writeFileSync('pr-meta/already-notified.txt', String(alreadyNotified));
core.setOutput('pr_number', pr.number);
- name: Check review readiness
run: python lib/checks/check-review-ready.py
- name: Post notification
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const actionFile = 'pr-meta/action.txt';
if (!fs.existsSync(actionFile)) return;
const action = fs.readFileSync(actionFile, 'utf8').trim();
if (action !== 'notify') {
console.log('Not ready for review — skipping.');
return;
}
const body = [
'<!-- pr-review-ready -->',
'This PR is now ready to be merged, pending maintainer review. All checks are passing and it has been peer-reviewed.',
'',
'@Lissy93 - Please evaluate, and either merge or leave feedback.',
].join('\n');
const prNumber = parseInt('${{ steps.context.outputs.pr_number }}');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
// Once all checks pass, assign the PR to myself, so i get notiifcation to review
try {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
assignees: ['Lissy93'],
});
} catch (err) {
console.log(`Failed to assign Lissy93: ${err.message}`);
}
# Applies a label matching the "Type" picked in the PR template. Non-critical and
# fully self-contained: it never fails the run, and runs independently of the checks.
label-type:
name: Apply type label
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
permissions:
issues: write
pull-requests: write
concurrency:
group: label-type-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Apply label from PR type
continue-on-error: true
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
try {
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
// Template "Type" token -> label to apply
const TYPES = [
['Addition', 'Addition'],
['Amendment', 'Amendment'],
['Removal', 'Removal'],
['Spelling or Grammar', 'Grammar'],
['Website', 'Website'],
['Scripts', 'Scripts'],
['Misc', 'Misc'],
];
const managed = [...TYPES.map(t => t[1]), 'Uncategorized'];
// Isolate the "### Type" section, drop HTML comments, lowercase for matching
const body = pr.body || '';
const m = body.match(/###\s*Type\b([\s\S]*?)(?:\n\s*###|\n\s*---|$)/i);
const section = (m ? m[1] : '').replace(/<!--[\s\S]*?-->/g, '').toLowerCase();
// A correctly filled template leaves exactly one type; otherwise Uncategorized
const matched = TYPES.filter(t => section.includes(t[0].toLowerCase())).map(t => t[1]);
const desired = matched.length === 1 ? matched[0] : 'Uncategorized';
const current = (pr.labels || []).map(l => l.name);
for (const name of current.filter(l => managed.includes(l) && l !== desired)) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name })
.catch(e => console.log(`Skip remove ${name}: ${e.message}`));
}
if (!current.includes(desired)) {
await github.rest.issues.getLabel({ owner, repo, name: desired }).catch(async e => {
if (e.status === 404) {
await github.rest.issues.createLabel({ owner, repo, name: desired, color: 'ededed' });
}
});
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [desired] });
}
console.log(`Type label: ${desired} (matched ${matched.length})`);
} catch (err) {
console.log(`Type labeling skipped: ${err.message}`);
}