From 6b809fb7540e79a0ae88cc361e3a8b22c3e01428 Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Thu, 5 Mar 2026 12:13:20 +0100 Subject: [PATCH 1/5] 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). --- .github/workflows/verified-approvals.yml | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/verified-approvals.yml diff --git a/.github/workflows/verified-approvals.yml b/.github/workflows/verified-approvals.yml new file mode 100644 index 00000000..721c8ed6 --- /dev/null +++ b/.github/workflows/verified-approvals.yml @@ -0,0 +1,86 @@ +--- +name: Verified Approvals + +on: + # pull_request_target: + pull_request: + merge_group: + +jobs: + verified-approvals: + name: verified-approvals + runs-on: github-ubuntu-latest-s + permissions: + id-token: write + pull-requests: read + steps: + - id: secrets + if: github.event_name != 'merge_group' + uses: SonarSource/vault-action-wrapper@3d5c87cb535e4a2c7a09adcbcfdefa751854dee3 # 3.3.0 + with: + # use -approvals after https://github.com/SonarSource/re-terraform-aws-vault/pull/8741 merge + # development/github/token/SonarSource-sonar-dummy-python-oss-jira is manually tweaked for use in tests + secrets: | + development/github/token/{REPO_OWNER_NAME_DASH}-jira token | ORG_TOKEN; + - name: Check approvals + if: github.event_name != 'merge_group' + env: + GH_TOKEN: ${{ github.token }} + ORG_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ORG_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + ORG: SonarSource + run: | + is_external=false + # A PR is external if it comes from a fork OR contains commits from non-org members + if [[ "${IS_FORK}" == "true" ]]; then + echo "PR is from a fork: treating as external" + is_external=true + else + echo "PR is not from a fork: checking commit committers..." + # Check commit committers (who pushed) — bot accounts (type Bot) are excluded + mapfile -t commit_authors < <( + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits" --paginate \ + | jq -rs 'add // [] | [.[].committer | select(. != null) | select(.type == "User") | .login] | unique | .[]' + ) + for login in "${commit_authors[@]}"; do + if ! GH_TOKEN="${ORG_TOKEN}" gh api "orgs/${ORG}/members/${login}" --silent 2>/dev/null; then + echo "External contribution: commit by non-org member '${login}'" + is_external=true + break + fi + done + fi + + if [[ "${is_external}" == "true" ]]; then + required=2 + echo "External PR: requiring ${required} org-member approvals" + else + required=1 + echo "Internal PR: requiring ${required} org-member approval(s)" + fi + + # Collect logins with a net APPROVED state (latest review per user) + mapfile -t approved_logins < <( + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ + | jq -rs 'add // [] | [group_by(.user.login)[] | last | select(.state == "APPROVED") | .user.login] | .[]' + ) + count=0 + for login in "${approved_logins[@]}"; do + # Only count approvals from org members (requires read:org token) + if GH_TOKEN="${ORG_TOKEN}" gh api "orgs/${ORG}/members/${login}" --silent 2>/dev/null; then + echo " ${login}: org member ✓" + (( count++ )) || true + else + echo " ${login}: not an org member, ignored" + fi + done + echo "Org-member approvals: ${count} / ${required} required" + + if (( count >= required )); then + echo "::notice ::Check passed: ${count} org-member approval(s)" + exit 0 + else + echo "::error ::Check failed: ${count} org-member approval(s), ${required} required" >&2 + exit 1 + fi From 44e3f664a2c7ff260c6e517bb9ffd02d7a98005a Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Thu, 5 Mar 2026 12:13:24 +0100 Subject: [PATCH 2/5] BUILD-10590 Add re-trigger approvals workflow --- .github/workflows/re-trigger-approvals.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/re-trigger-approvals.yml diff --git a/.github/workflows/re-trigger-approvals.yml b/.github/workflows/re-trigger-approvals.yml new file mode 100644 index 00000000..d04ef4c7 --- /dev/null +++ b/.github/workflows/re-trigger-approvals.yml @@ -0,0 +1,21 @@ +on: + workflow_call: + inputs: + head_sha: + type: string + required: true + +jobs: + re-trigger: + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ inputs.head_sha }} + run: | + run_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request&head_sha=${HEAD_SHA}" \ + --jq '.workflow_runs[] | select(.name == "Verified Approvals") | .id' \ + | head -1) + [[ -n "${run_id}" ]] && gh api --method POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/rerun" From 5aea89517aaaac142c58dba1d389c9f925e1fdeb Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Fri, 6 Mar 2026 14:11:46 +0100 Subject: [PATCH 3/5] BUILD-10590 Also check commit authors (not only committers) for external contribution detection Bot accounts (type Bot) remain excluded and treated as internal. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/re-trigger-approvals.yml | 2 +- .github/workflows/verified-approvals.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/re-trigger-approvals.yml b/.github/workflows/re-trigger-approvals.yml index d04ef4c7..ec6aa627 100644 --- a/.github/workflows/re-trigger-approvals.yml +++ b/.github/workflows/re-trigger-approvals.yml @@ -7,7 +7,7 @@ on: jobs: re-trigger: - runs-on: ubuntu-latest + runs-on: github-ubuntu-latest-s permissions: actions: write steps: diff --git a/.github/workflows/verified-approvals.yml b/.github/workflows/verified-approvals.yml index 721c8ed6..2a04f259 100644 --- a/.github/workflows/verified-approvals.yml +++ b/.github/workflows/verified-approvals.yml @@ -38,10 +38,10 @@ jobs: is_external=true else echo "PR is not from a fork: checking commit committers..." - # Check commit committers (who pushed) — bot accounts (type Bot) are excluded + # Check commit authors and committers (who wrote and who pushed) — bot accounts (type Bot) are excluded mapfile -t commit_authors < <( gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/commits" --paginate \ - | jq -rs 'add // [] | [.[].committer | select(. != null) | select(.type == "User") | .login] | unique | .[]' + | jq -rs 'add // [] | [.[] | (.author, .committer) | select(. != null) | select(.type == "User") | .login] | unique | .[]' ) for login in "${commit_authors[@]}"; do if ! GH_TOKEN="${ORG_TOKEN}" gh api "orgs/${ORG}/members/${login}" --silent 2>/dev/null; then From 2e9f6f184fe85d2b87b0a333fddf83ddb6b4713b Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Fri, 6 Mar 2026 14:55:05 +0100 Subject: [PATCH 4/5] BUILD-10590 use org secret --- .github/workflows/verified-approvals.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/verified-approvals.yml b/.github/workflows/verified-approvals.yml index 2a04f259..bcc64743 100644 --- a/.github/workflows/verified-approvals.yml +++ b/.github/workflows/verified-approvals.yml @@ -11,22 +11,13 @@ jobs: name: verified-approvals runs-on: github-ubuntu-latest-s permissions: - id-token: write pull-requests: read steps: - - id: secrets - if: github.event_name != 'merge_group' - uses: SonarSource/vault-action-wrapper@3d5c87cb535e4a2c7a09adcbcfdefa751854dee3 # 3.3.0 - with: - # use -approvals after https://github.com/SonarSource/re-terraform-aws-vault/pull/8741 merge - # development/github/token/SonarSource-sonar-dummy-python-oss-jira is manually tweaked for use in tests - secrets: | - development/github/token/{REPO_OWNER_NAME_DASH}-jira token | ORG_TOKEN; - name: Check approvals if: github.event_name != 'merge_group' env: GH_TOKEN: ${{ github.token }} - ORG_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ORG_TOKEN }} + ORG_TOKEN: ${{ secrets.PR_APPROVALS }} PR_NUMBER: ${{ github.event.pull_request.number }} IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} ORG: SonarSource From 5e0ac85b64112106c4732959450a8b2fb9db7ebe Mon Sep 17 00:00:00 2001 From: Julien Carsique Date: Fri, 6 Mar 2026 15:37:38 +0100 Subject: [PATCH 5/5] BUILD-10590 need pull_request_target event --- .github/workflows/re-trigger-approvals.yml | 2 +- .github/workflows/verified-approvals.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/re-trigger-approvals.yml b/.github/workflows/re-trigger-approvals.yml index ec6aa627..132320cc 100644 --- a/.github/workflows/re-trigger-approvals.yml +++ b/.github/workflows/re-trigger-approvals.yml @@ -15,7 +15,7 @@ jobs: GH_TOKEN: ${{ github.token }} HEAD_SHA: ${{ inputs.head_sha }} run: | - run_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request&head_sha=${HEAD_SHA}" \ + run_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request_target&head_sha=${HEAD_SHA}" \ --jq '.workflow_runs[] | select(.name == "Verified Approvals") | .id' \ | head -1) [[ -n "${run_id}" ]] && gh api --method POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/rerun" diff --git a/.github/workflows/verified-approvals.yml b/.github/workflows/verified-approvals.yml index bcc64743..b55a6bce 100644 --- a/.github/workflows/verified-approvals.yml +++ b/.github/workflows/verified-approvals.yml @@ -2,8 +2,7 @@ name: Verified Approvals on: - # pull_request_target: - pull_request: + pull_request_target: merge_group: jobs: