Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/contributor-check-external-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: agentrust-io/.github
ref: main
# Pinned to a specific commit of agentrust-io/.github: this workflow
# runs on pull_request_target with a write-capable token, so the
# code it pulls in must not be a mutable ref. Bump this SHA
# deliberately (after review) when the org action changes.
ref: 8a2b77bc35057566fd675f0a24e814564cf5322f # main as of 2026-06-10
persist-credentials: false
sparse-checkout: |
scripts
Expand Down
55 changes: 46 additions & 9 deletions .github/workflows/require-maintainer-approval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ on:
pull_request_target:
types: [opened, synchronize, reopened]
branches: [main]
# Re-evaluate the gate when a review is submitted or dismissed so an
# approval clears the failing status without waiting for a new push.
# Note: pull_request_review does not support a branches filter, so the
# base-branch check lives in the job-level `if` below.
pull_request_review:
types: [submitted, dismissed]

permissions:
contents: read
Expand All @@ -19,6 +25,7 @@ jobs:
contents: read
pull-requests: read
if: >-
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.author_association != 'MEMBER' &&
github.event.pull_request.author_association != 'OWNER'
steps:
Expand All @@ -28,25 +35,55 @@ jobs:
script: |
const MAINTAINERS = ['imran-siddique'];

const reviews = await github.rest.pulls.listReviews({
// Both pull_request_target and pull_request_review payloads
// carry the PR at context.payload.pull_request.
const prNumber = context.payload.pull_request.number;

// Re-fetch the PR so we compare against the head SHA at
// evaluation time, not a possibly stale SHA from the payload.
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const headSha = pr.head.sha;

// Paginate: listReviews otherwise returns only the first 30.
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
pull_number: prNumber,
});

const maintainerApproval = reviews.data.find(
(r) =>
r.state === 'APPROVED' &&
MAINTAINERS.includes(r.user.login) &&
r.user.type === 'User'
// Only a maintainer's most recent non-comment review counts,
// and it must approve the current head commit. An approval of
// an older commit does not clear the gate after a later push
// (prevents approve-then-swap).
const latestByMaintainer = new Map();
for (const review of reviews) {
if (
review.user &&
review.user.type === 'User' &&
MAINTAINERS.includes(review.user.login) &&
review.state !== 'COMMENTED'
) {
latestByMaintainer.set(review.user.login, review);
}
}

const maintainerApproval = [...latestByMaintainer.values()].find(
(r) => r.state === 'APPROVED' && r.commit_id === headSha
);

if (!maintainerApproval) {
core.setFailed(
'This PR is awaiting a named-maintainer review.\n' +
'This PR is awaiting a named-maintainer review of the current head commit.\n' +
'This is a policy gate, not a CI failure: the PR is mergeable once a named maintainer approves.\n' +
'Approvals of earlier commits do not count after new pushes.\n' +
'See CODEOWNERS or MAINTAINERS.md for the list of maintainers.'
);
} else {
core.info(`Maintainer @${maintainerApproval.user.login} approved this PR.`);
core.info(
`Maintainer @${maintainerApproval.user.login} approved this PR at head ${headSha}.`
);
}
Loading