Skip to content

feat(local): highlight JSON with twinkleplop #270

feat(local): highlight JSON with twinkleplop

feat(local): highlight JSON with twinkleplop #270

Workflow file for this run

name: PR Risk (Jev)

Check warning on line 1 in .github/workflows/pr-risk-jev.yml

View workflow run for this annotation

GitHub Actions / PR Risk (Jev)

Workflow execution policy warning (evaluate mode)

On November 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, edited]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to analyze
required: true
type: number
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-risk-jev-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
risk:
name: PR risk (Jev)
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
steps:
# The Action reads PR Git objects without checking out or executing PR code.
- name: Classify PR risk
id: risk
uses: getsentry/pr-risk-action@45de582ca688251eab50b4c7cea096d46756cd4d
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
with:
repo: ${{ github.repository }}
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}
expected-head-sha: ${{ github.event.pull_request.head.sha }}
result-path: risk-pr-result.json
- name: Publish risk label
if: ${{ !cancelled() && steps.risk.outcome != 'skipped' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
with:
script: |
// Only this trusted workflow publishes labels; PR source is never executed.
const fs = require('node:fs');
if (!fs.existsSync('risk-pr-result.json')) {
core.warning('Risk analysis did not produce a result; no label was published.');
return;
}
const result = JSON.parse(fs.readFileSync('risk-pr-result.json', 'utf8'));
const prNumber = Number(process.env.PR_NUMBER);
const repo = context.repo;
const colors = { low: '0E8A16', medium: 'FBCA04', high: 'B60205' };
const riskLabels = Object.keys(colors).map(risk => `risk: ${risk}`);
if (!Number.isSafeInteger(prNumber) || prNumber <= 0
|| result.number !== prNumber || result.repo !== `${repo.owner}/${repo.repo}`
|| typeof result.status !== 'string'
|| (result.status === 'ok' && !Object.hasOwn(colors, result.risk_label))) {
throw new Error('Risk result does not match this PR or a valid risk class.');
}
const snapshot = result.snapshot;
if (result.status === 'pr_changed' || !snapshot?.source_head_sha || !snapshot?.source_base_sha) {
core.warning('Risk analysis has no current revision; no label was published.');
return;
}
const { data: pr } = await github.rest.pulls.get({ ...repo, pull_number: prNumber });
if (pr.state !== 'open' || pr.head.sha !== snapshot.source_head_sha
|| pr.base.sha !== snapshot.source_base_sha) {
core.warning('PR changed after risk analysis; no label was published.');
return;
}
const target = result.status === 'ok' ? `risk: ${result.risk_label}` : null;
const existing = pr.labels.map(label => label.name);
if (target && !existing.includes(target)) {
try {
await github.rest.issues.getLabel({ ...repo, name: target });
} catch (error) {
if (error.status !== 404) throw error;
try {
await github.rest.issues.createLabel({
...repo, name: target, color: colors[result.risk_label],
description: `PR risk score: ${result.risk_label}`,
});
} catch (error) {
if (error.status !== 422) throw error;
// Another PR may have created the shared repository label.
await github.rest.issues.getLabel({ ...repo, name: target });
}
}
await github.rest.issues.addLabels({ ...repo, issue_number: prNumber, labels: [target] });
}
// A failed classification must not leave an earlier low/medium/high verdict.
for (const name of existing.filter(name => riskLabels.includes(name) && name !== target)) {
try {
await github.rest.issues.removeLabel({ ...repo, issue_number: prNumber, name });
} catch (error) {
if (error.status !== 404) throw error;
}
}
const { data: latest } = await github.rest.pulls.get({ ...repo, pull_number: prNumber });
if (target && (latest.state !== 'open' || latest.head.sha !== pr.head.sha
|| latest.base.sha !== pr.base.sha || latest.title !== pr.title || latest.body !== pr.body)) {
await github.rest.issues.removeLabel({ ...repo, issue_number: prNumber, name: target });
core.warning('PR changed while publishing; removed the outdated risk label.');
return;
}
if (!target) core.warning(`PR remains unclassified: ${result.status}. Previous risk labels were cleared.`);
core.info(target ? `Published ${target}` : 'No risk label published.');
- name: Upload risk result
if: always()
uses: actions/upload-artifact@v7
with:
name: pr-risk-jev-${{ github.event.pull_request.number || inputs.pr_number }}
path: risk-pr-result.json
if-no-files-found: warn
retention-days: 30