Skip to content

Commit 6b809fb

Browse files
BUILD-10590 Add verified-approvals required workflow
Checks PR approvals to enforce the SonarSource org ruleset: - 1 approval required for internal PRs - 2 approvals required for external (fork) PRs Triggers on pull_request and pull_request_review (submitted/dismissed) events. Skips approval check for merge_group events (always pass). Considers a PR as external if it contains commits from non-org members (in addition to the fork check); bot accounts are excluded. Uses committer login (who pushed) instead of declared author (which can be faked). Uses a dedicated {REPO_OWNER_NAME_DASH}-approvals vault token (members:read) for all org membership checks (commit committers and PR approvers).
1 parent 56a7ed3 commit 6b809fb

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
name: Verified Approvals
3+
4+
on:
5+
# pull_request_target:
6+
pull_request:
7+
merge_group:
8+
9+
jobs:
10+
verified-approvals:
11+
name: verified-approvals
12+
runs-on: github-ubuntu-latest-s
13+
permissions:
14+
id-token: write
15+
pull-requests: read
16+
steps:
17+
- id: secrets
18+
if: github.event_name != 'merge_group'
19+
uses: SonarSource/vault-action-wrapper@3d5c87cb535e4a2c7a09adcbcfdefa751854dee3 # 3.3.0
20+
with:
21+
# use -approvals after https://github.com/SonarSource/re-terraform-aws-vault/pull/8741 merge
22+
# development/github/token/SonarSource-sonar-dummy-python-oss-jira is manually tweaked for use in tests
23+
secrets: |
24+
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | ORG_TOKEN;
25+
- name: Check approvals
26+
if: github.event_name != 'merge_group'
27+
env:
28+
GH_TOKEN: ${{ github.token }}
29+
ORG_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ORG_TOKEN }}
30+
PR_NUMBER: ${{ github.event.pull_request.number }}
31+
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
32+
ORG: SonarSource
33+
run: |
34+
is_external=false
35+
# A PR is external if it comes from a fork OR contains commits from non-org members
36+
if [[ "${IS_FORK}" == "true" ]]; then
37+
echo "PR is from a fork: treating as external"
38+
is_external=true
39+
else
40+
echo "PR is not from a fork: checking commit committers..."
41+
# Check commit committers (who pushed) — bot accounts (type Bot) are excluded
42+
mapfile -t commit_authors < <(
43+
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits" --paginate \
44+
| jq -rs 'add // [] | [.[].committer | select(. != null) | select(.type == "User") | .login] | unique | .[]'
45+
)
46+
for login in "${commit_authors[@]}"; do
47+
if ! GH_TOKEN="${ORG_TOKEN}" gh api "orgs/${ORG}/members/${login}" --silent 2>/dev/null; then
48+
echo "External contribution: commit by non-org member '${login}'"
49+
is_external=true
50+
break
51+
fi
52+
done
53+
fi
54+
55+
if [[ "${is_external}" == "true" ]]; then
56+
required=2
57+
echo "External PR: requiring ${required} org-member approvals"
58+
else
59+
required=1
60+
echo "Internal PR: requiring ${required} org-member approval(s)"
61+
fi
62+
63+
# Collect logins with a net APPROVED state (latest review per user)
64+
mapfile -t approved_logins < <(
65+
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \
66+
| jq -rs 'add // [] | [group_by(.user.login)[] | last | select(.state == "APPROVED") | .user.login] | .[]'
67+
)
68+
count=0
69+
for login in "${approved_logins[@]}"; do
70+
# Only count approvals from org members (requires read:org token)
71+
if GH_TOKEN="${ORG_TOKEN}" gh api "orgs/${ORG}/members/${login}" --silent 2>/dev/null; then
72+
echo " ${login}: org member ✓"
73+
(( count++ )) || true
74+
else
75+
echo " ${login}: not an org member, ignored"
76+
fi
77+
done
78+
echo "Org-member approvals: ${count} / ${required} required"
79+
80+
if (( count >= required )); then
81+
echo "::notice ::Check passed: ${count} org-member approval(s)"
82+
exit 0
83+
else
84+
echo "::error ::Check failed: ${count} org-member approval(s), ${required} required" >&2
85+
exit 1
86+
fi

0 commit comments

Comments
 (0)