Skip to content

Fork SonarCloud (manual approval) #223

Fork SonarCloud (manual approval)

Fork SonarCloud (manual approval) #223

Workflow file for this run

# SonarCloud analysis for pull requests from forks (manual maintainer gate).
#
# The unprivileged Java CI workflow performs the build, generated-source check,
# and tests before this workflow can run. This workflow restores the resulting
# analysis inputs and publishes them to SonarCloud; it does not rebuild or
# retest fork code with the Sonar token.
#
# It runs on `workflow_run` in the base-repo context (with secrets), and is
# protected by the `fork-ci` GitHub Environment with Required reviewers.
# Do NOT remove that environment gate.
#
# Security model (see docs/CONTRIBUTING.md, "CI for pull requests from forks"):
# nothing fork-controlled is ever *executed* here. Maven only runs against the
# trusted base-branch checkout (to resolve the dependency classpath); the fork
# PR head is then fetched as plain git objects and analysed by the standalone
# Sonar scanner, whose configuration is written by this workflow and passed via
# `project.settings`, so a `sonar-project.properties`, `pom.xml`, `.mvn/`, or
# wrapper script in the fork tree is inert data. Do NOT reintroduce a Maven
# invocation (or any build tool) on the fork tree in this workflow.
name: Fork SonarCloud (manual approval)
on:
workflow_run:
workflows: ["Java CI with Maven"]
types: [completed]
permissions:
contents: read
actions: read
concurrency:
group: fork-sonar-${{ github.event.workflow_run.id }}
cancel-in-progress: true
jobs:
sonar:
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
runs-on: ubuntu-latest
timeout-minutes: 20
environment: fork-ci
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- name: Download successful fork PR analysis inputs
# Store outside the workspace so the later checkout (which cleans the
# workspace) does not remove these files.
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sonar-analysis-inputs
path: ${{ runner.temp }}/sonar-analysis-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Read PR metadata
id: meta
# The payload file is produced by the untrusted fork build, so extract
# only the expected keys and validate their format before exposing them
# as step outputs (prevents $GITHUB_OUTPUT injection).
run: |
env_file="$RUNNER_TEMP/sonar-analysis-data/pr-event.env"
pr_number=$(grep -m1 '^pr_number=' "$env_file" | cut -d= -f2-)
pr_head_sha=$(grep -m1 '^pr_head_sha=' "$env_file" | cut -d= -f2-)
pr_head_ref=$(grep -m1 '^pr_head_ref=' "$env_file" | cut -d= -f2-)
pr_base_ref=$(grep -m1 '^pr_base_ref=' "$env_file" | cut -d= -f2-)
[[ "$pr_number" =~ ^[0-9]+$ ]] || { echo "invalid pr_number"; exit 1; }
[[ "$pr_head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "invalid pr_head_sha"; exit 1; }
[[ "$pr_head_ref" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "invalid pr_head_ref"; exit 1; }
[[ "$pr_base_ref" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "invalid pr_base_ref"; exit 1; }
{
echo "pr_number=$pr_number"
echo "pr_head_sha=$pr_head_sha"
echo "pr_head_ref=$pr_head_ref"
echo "pr_base_ref=$pr_base_ref"
} >> "$GITHUB_OUTPUT"
- name: Check out trusted base branch
# No `ref`: this is the base repository's default branch, i.e. trusted
# code. It is the only tree Maven ever runs against in this workflow.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: '17'
distribution: 'zulu'
cache: maven
- name: Cache SonarCloud packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-sonar
- name: Resolve dependency classpath from the trusted base pom
# The Java analyzer needs the dependency jars for full type resolution.
# Resolve them from the base branch's pom (not the fork's) so that the
# only pom Maven parses is trusted. A dependency added by the fork PR
# is simply unresolved in the analysis; it is never downloaded here.
run: |
mvn -B -q -DskipTests -Dfmt.skip -pl core install
mvn -B -q -pl processor dependency:build-classpath \
-Dmdep.includeScope=test \
-Dmdep.pathSeparator=, \
-DexcludeGroupIds=io.github.java-helpers \
-Dmdep.outputFile="$RUNNER_TEMP/sonar-libraries.txt"
- name: Fetch fork PR source as git data (not executed)
# The PR head exists in the base repo as refs/pull/N/head. Fetching and
# checking it out only writes files and gives Sonar the SCM history it
# needs to compute changed lines; nothing in the fork tree is run. The
# checked-out commit must be the one the analysis inputs were built from.
env:
PR_NUMBER: ${{ steps.meta.outputs.pr_number }}
PR_HEAD_SHA: ${{ steps.meta.outputs.pr_head_sha }}
run: |
git fetch --no-tags origin "refs/pull/$PR_NUMBER/head"
[ "$(git rev-parse FETCH_HEAD)" = "$PR_HEAD_SHA" ] || {
echo "::error::PR head $(git rev-parse FETCH_HEAD) does not match analysed commit $PR_HEAD_SHA"
exit 1
}
git -c advice.detachedHead=false checkout --detach FETCH_HEAD
- name: Restore compiled classes and coverage reports
run: |
src="$RUNNER_TEMP/sonar-analysis-data"
mkdir -p core/target/classes processor/target/classes
mkdir -p core/target/test-classes processor/target/test-classes
cp -a "$src/core-classes/." core/target/classes/ 2>/dev/null || true
cp -a "$src/processor-classes/." processor/target/classes/ 2>/dev/null || true
cp -a "$src/core-test-classes/." core/target/test-classes/ 2>/dev/null || true
cp -a "$src/processor-test-classes/." processor/target/test-classes/ 2>/dev/null || true
mkdir -p processor/target/site/jacoco processor/target/site/jacoco-aggregate
cp -a "$src/processor-jacoco.xml" processor/target/site/jacoco/jacoco.xml 2>/dev/null || true
cp -a "$src/jacoco-aggregate.xml" processor/target/site/jacoco-aggregate/jacoco.xml 2>/dev/null || true
- name: Write scanner configuration
# Written outside the workspace and selected via `project.settings`, so
# a sonar-project.properties in the fork tree is never read.
# Keep the sonar.* values in sync with the <properties> in pom.xml.
run: |
libraries="$(cat "$RUNNER_TEMP/sonar-libraries.txt")"
cat > "$RUNNER_TEMP/sonar-project.properties" <<EOF
sonar.projectKey=java-helpers_simple-builders
sonar.organization=java-helpers
sonar.projectName=Simple Builders
sonar.sources=core/src/main/java,processor/src/main/java
sonar.tests=core/src/test/java,processor/src/test/java
sonar.java.source=17
sonar.java.binaries=core/target/classes,processor/target/classes
sonar.java.test.binaries=core/target/test-classes,processor/target/test-classes
sonar.java.libraries=$libraries
sonar.java.test.libraries=$libraries
sonar.coverage.jacoco.xmlReportPaths=processor/target/site/jacoco-aggregate/jacoco.xml,processor/target/site/jacoco/jacoco.xml
sonar.exclusions=**/example/**
sonar.coverage.exclusions=**/processor/model/**,**/exceptions/**,**/generated/**
sonar.scm.provider=git
EOF
- name: Publish fork PR analysis to SonarCloud
if: env.SONAR_TOKEN != ''
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_HOST_URL: https://sonarcloud.io
with:
args: >
-Dproject.settings=${{ runner.temp }}/sonar-project.properties
-Dsonar.pullrequest.key=${{ steps.meta.outputs.pr_number }}
-Dsonar.pullrequest.branch=${{ steps.meta.outputs.pr_head_ref }}
-Dsonar.pullrequest.base=${{ steps.meta.outputs.pr_base_ref }}