Skip to content

design: help text for content fields #1729

design: help text for content fields

design: help text for content fields #1729

Workflow file for this run

name: Approval

Check warning on line 1 in .github/workflows/approval.yml

View workflow run for this annotation

GitHub Actions / Approval

Workflow execution policy warning (evaluate mode)

On December 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
# Maintains the informational `Human Maintainer approval` check and the four mutually
# exclusive review/* labels from one calculation, so a label never disagrees
# with the check. Pure actions/github-script: no checkout, no secrets, no agent.
#
# Do not add a pull_request_review trigger: it would run the PR branch's copy of
# this workflow. Instead the reviewer re-applies the PR's review label from the
# review webhook (infra/flue-review/.flue/lib/review-state.ts), and the labeled
# event runs the default branch's copy. Label changes made here use GITHUB_TOKEN
# and do not trigger it again.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft, labeled]
push:
# Re-evaluate every open PR when the rules or the Maintainers list change.
branches: [main]
paths:
- ".github/workflows/approval.yml"
- "GOVERNANCE.md"
schedule:
# Backstop sweep so the check and labels stay correct if an event was missed.
- cron: "15 */6 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
approval:
name: Update human review status
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
checks: write
contents: read
pull-requests: write
concurrency:
group: approval-${{ github.event.pull_request.number || 'sweep' }}
cancel-in-progress: false
steps:
- name: Update approval check and review labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const checkName = 'Human Maintainer approval';
const detailsUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const stateNames = [
'review/needs-review',
'review/awaiting-author',
'review/needs-rereview',
'review/approved',
];
const privilegedPaths = [
/^GOVERNANCE\.md$/,
/^MAINTAINING\.md$/,
/^AI_POLICY\.md$/,
/^CONTRIBUTING\.md$/,
/^TRIAGE\.md$/,
/^AGENTS\.md$/,
/^SECURITY\.md$/,
/^proposals\/(README|feature-plan-template|rfc-template)\.md$/,
/^rfcs\/README\.md$/,
/^\.github\/CODEOWNERS$/,
/^\.github\/PULL_REQUEST_TEMPLATE(?:\.md|\/)/,
/^\.github\/ISSUE_TEMPLATE\//,
/^\.github\/DISCUSSION_TEMPLATE\//,
/^\.github\/dependabot\.yml$/,
/^\.github\/codeql-config\.yml$/,
/^\.github\/zizmor\.yml$/,
/^\.github\/workflows\//,
/^\.github\/actions\//,
/^\.github\/scripts\//,
/^\.changeset\/config\.json$/,
/^renovate\.json$/,
/^pnpm-workspace\.yaml$/,
/^apps\/release-action\//,
/^apps\/release-service\//,
/^apps\/release-verifier\//,
/^infra\/flue-review\//,
];
// Read from the default branch, so neither a PR nor its base branch can
// change who counts as a Maintainer.
let governanceFile;
try {
({ data: governanceFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'GOVERNANCE.md',
}));
} catch (error) {
if (error.status !== 404) throw error;
core.notice('GOVERNANCE.md is not on the default branch, so no approval can be evaluated.');
return;
}
if (Array.isArray(governanceFile) || governanceFile.type !== 'file') {
core.setFailed('GOVERNANCE.md was not returned as a file.');
return;
}
const governance = Buffer.from(governanceFile.content, 'base64').toString('utf8');
const maintainerBlock = governance.match(
/<!-- maintainers:start -->([\s\S]*?)<!-- maintainers:end -->/,
)?.[1];
const maintainers = new Set(
[...(maintainerBlock || '').matchAll(/https:\/\/github\.com\/([a-z\d-]+)/gi)]
.map(match => match[1].toLowerCase()),
);
if (maintainers.size === 0) {
core.setFailed('GOVERNANCE.md does not contain a marked Maintainers list.');
return;
}
async function setReviewLabel(pr, desired) {
const current = new Set((pr.labels || []).map(label => label.name));
if (desired && !current.has(desired)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: [desired],
});
}
for (const name of stateNames) {
if (name === desired || !current.has(name)) continue;
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr.number,
name,
});
} catch (error) {
if (error.status !== 404) throw error;
}
}
}
async function setApprovalStatus(pr, approved, title, summary) {
const runs = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: pr.head.sha,
check_name: checkName,
filter: 'all',
per_page: 100,
});
const checks = runs.filter(run => run.name === checkName);
const check = checks[0];
const output = { title, summary };
const outcome = {
status: 'completed',
conclusion: approved ? 'success' : 'neutral',
completed_at: new Date().toISOString(),
};
if (check) {
await github.rest.checks.update({
owner,
repo,
check_run_id: check.id,
...outcome,
details_url: detailsUrl,
output,
});
} else {
await github.rest.checks.create({
owner,
repo,
name: checkName,
head_sha: pr.head.sha,
...outcome,
details_url: detailsUrl,
output,
});
}
for (const older of checks.slice(1)) {
if (older.status === 'completed') continue;
await github.rest.checks.update({
owner,
repo,
check_run_id: older.id,
...outcome,
details_url: detailsUrl,
output,
});
}
}
async function evaluate(pr) {
if (pr.draft) {
await setApprovalStatus(
pr,
false,
'Draft pull request',
'Maintainer review is not requested while this pull request is a draft.',
);
await setReviewLabel(pr, null);
core.info(`#${pr.number}: draft, no approval required yet`);
return;
}
const [reviews, commits, files] = await Promise.all([
github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
}),
github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
}),
github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
}),
]);
// A review covers the current revision when every later commit is a
// merge commit that updates the branch.
const commitIndex = new Map(commits.map((commit, index) => [commit.sha, index]));
const coversCurrentRevision = review => {
const reviewedIndex = commitIndex.get(review.commit_id);
if (reviewedIndex === undefined) return false;
return commits
.slice(reviewedIndex + 1)
.every(commit => (commit.parents || []).length > 1);
};
const maintainerReviews = reviews
.filter(review =>
review.user &&
review.user.type !== 'Bot' &&
review.user.login !== pr.user.login &&
maintainers.has(review.user.login.toLowerCase()) &&
Number.isFinite(Date.parse(review.submitted_at || ''))
)
.toSorted((a, b) => Date.parse(a.submitted_at) - Date.parse(b.submitted_at));
const latestDecisionByReviewer = new Map();
let lastReview = null;
for (const review of maintainerReviews) {
if (['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state)) {
latestDecisionByReviewer.set(review.user.login, review);
}
if (['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED'].includes(review.state)) {
lastReview = review;
}
}
const decisions = [...latestDecisionByReviewer.values()];
const approvers = decisions
.filter(review => review.state === 'APPROVED' && coversCurrentRevision(review))
.map(review => review.user.login);
const changeRequests = decisions.filter(review => review.state === 'CHANGES_REQUESTED');
const privileged = files.some(file =>
privilegedPaths.some(pattern => pattern.test(file.filename))
);
const required = privileged ? 2 : 1;
const met = changeRequests.length === 0 && approvers.length >= required;
let label;
if (met) {
label = 'review/approved';
} else if (!lastReview) {
label = 'review/needs-review';
} else if (
!coversCurrentRevision(lastReview) ||
changeRequests.some(review => !coversCurrentRevision(review))
) {
label = 'review/needs-rereview';
} else if (changeRequests.length > 0 || lastReview.state === 'COMMENTED') {
label = 'review/awaiting-author';
} else {
label = 'review/needs-review';
}
if (met) {
await setApprovalStatus(
pr,
true,
'Human Maintainer approval requirement met',
`Current revision approved by ${approvers.join(', ')}.`,
);
} else if (changeRequests.length > 0) {
const requesters = changeRequests.map(review => review.user.login);
await setApprovalStatus(
pr,
false,
'Changes requested by a human Maintainer',
`Current revision has ${approvers.length} of ${required} expected human Maintainer approvals. Changes are still requested by ${requesters.join(', ')}.`,
);
} else {
const category = privileged ? 'governance or privileged supply-chain' : 'normal';
await setApprovalStatus(
pr,
false,
'Awaiting human Maintainer approval',
`${category} changes require ${required} human Maintainer approval${required === 1 ? '' : 's'} on the current revision; found ${approvers.length}.`,
);
}
// Bot-authored PRs have a review status check but carry no review label. This
// also removes the label the reviewer adds to re-run the workflow.
await setReviewLabel(pr, pr.user.type === 'Bot' ? null : label);
core.info(`#${pr.number}: ${label}${met ? '' : ', approval pending'}`);
}
if (context.payload.pull_request) {
await evaluate(context.payload.pull_request);
return;
}
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
core.info(`Sweeping ${prs.length} open PR(s)`);
let failures = 0;
for (const pr of prs) {
try {
await evaluate(pr);
} catch (error) {
failures += 1;
core.warning(`#${pr.number}: failed to update approval state: ${error.message}`);
}
}
if (failures > 0) core.setFailed(`${failures} PR(s) could not be updated.`);