Skip to content
Open
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
155 changes: 155 additions & 0 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Reusable workflow: SonarQube Cloud analysis (code smells, bugs,
# security hotspots, duplication) with the quality gate as a status check.
#
# Only runs on public repositories, where SonarQube Cloud is free, and only
# on the default branch and on pull requests into the default branch. A
# private repository opts in with the Actions variable
# SONAR_SCAN_PRIVATE_REPO set to "true" (needs a paid SonarQube Cloud plan).
# Our subscription allows 50k lines of code for private repositories, so
# check the remaining allowance in SonarQube Cloud before opting in.
# On a private repository without that opt-in, on other branches (pushes,
# manual runs, pull requests into another branch), on merge_group events,
# and on pull requests that get no secrets (forks, Dependabot without a Dependabot secret), the job
# succeeds without analysing, so it can sit in an "all checks passed" gate.
# Scheduled runs analyse the default branch again with SonarQube Cloud's
# current rules.
#
# Setup per repository:
# 1. Add the repository to "Selected repositories" of the SonarQube Cloud
# GitHub App (organization Settings > GitHub Apps > SonarQube Cloud >
# Configure); otherwise SonarQube Cloud can't see it to import it.
# 2. Import the repository in the privacybydesign organization on
# sonarcloud.io and turn off Automatic Analysis (Administration >
# Analysis Method): SonarQube Cloud rejects CI results while it is on.
# 3. Make SONAR_TOKEN available to the repository (organization or
# repository secret, and as a Dependabot secret for Dependabot PRs).
#
# Required caller permissions: contents: read, pull-requests: read
# Required secret: SONAR_TOKEN

name: SonarQube

on:
workflow_call:
inputs:
organization:
description: SonarQube Cloud organization key.
type: string
default: privacybydesign
project-key:
description: SonarQube Cloud project key. Empty = <owner>_<repository>, the key SonarQube Cloud gives an imported repository.
type: string
default: ""
working-directory:
description: Analysis base directory (sonar.projectBaseDir).
type: string
default: .
exclusions:
description: >-
Comma-separated paths SonarQube does not analyse (sonar.exclusions), as
globs relative to working-directory, e.g. "**/testdata/**,docs/**". Empty = none.
type: string
default: ""
args:
description: Extra scanner arguments, e.g. "-Dsonar.go.coverage.reportPaths=coverage.out".
type: string
default: ""
quality-gate:
description: Fail the job when the quality gate fails (sonar.qualitygate.wait).
type: boolean
default: true
timeout-minutes:
type: number
default: 15
secrets:
SONAR_TOKEN:
required: false

permissions: {}

jobs:
sonarqube:
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
concurrency:
group: sonarqube-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
pull-requests: read
steps:
- name: Decide whether to analyse
id: gate
env:
PRIVATE: ${{ github.event.repository.private }}
SCAN_PRIVATE: ${{ vars.SONAR_SCAN_PRIVATE_REPO }}
EVENT: ${{ github.event_name }}
HAS_TOKEN: ${{ secrets.SONAR_TOKEN != '' }}
FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
ACTOR: ${{ github.actor }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
REF: ${{ github.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
run=true
if [ "$PRIVATE" = "true" ] && [ "$SCAN_PRIVATE" != "true" ]; then
echo "::notice::Private repository: SonarQube analysis skipped. Set the Actions variable SONAR_SCAN_PRIVATE_REPO to true to analyse it."
run=false
elif [ "$EVENT" = "merge_group" ]; then
echo "SonarQube analysis does not run on $EVENT events."
run=false
elif [ -n "$BASE_REF" ] && [ "$BASE_REF" != "$DEFAULT_BRANCH" ]; then
echo "Pull request targets $BASE_REF, not the default branch ($DEFAULT_BRANCH): SonarQube analysis skipped."
run=false
elif [ -z "$BASE_REF" ] && [ "$EVENT" != "schedule" ] && [ "$REF" != "refs/heads/$DEFAULT_BRANCH" ]; then
# Scheduled runs always use the default branch; their payload has no repository object.
echo "$REF is not the default branch ($DEFAULT_BRANCH): SonarQube analysis skipped."
run=false
elif [ "$HAS_TOKEN" != "true" ]; then
if [ "$EVENT" = "pull_request" ] && { [ "$FORK" = "true" ] || [ "$ACTOR" = "dependabot[bot]" ]; }; then
echo "::notice::No SONAR_TOKEN for this pull request (fork or Dependabot): SonarQube analysis skipped."
run=false
else
echo "::error::SONAR_TOKEN is not set. Add it as a secret and pass it to this workflow (secrets: SONAR_TOKEN)."
exit 1
fi
fi
echo "run=$run" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.gate.outputs.run == 'true'
with:
fetch-depth: 0 # blame information for new-code detection
persist-credentials: false

- name: Scanner arguments
if: steps.gate.outputs.run == 'true'
id: args
env:
ORGANIZATION: ${{ inputs.organization }}
PROJECT_KEY: ${{ inputs.project-key || format('{0}_{1}', github.repository_owner, github.event.repository.name) }}
QUALITY_GATE: ${{ inputs.quality-gate }}
EXCLUSIONS: ${{ inputs.exclusions }}
EXTRA_ARGS: ${{ inputs.args }}
run: |
args="-Dsonar.organization=$ORGANIZATION -Dsonar.projectKey=$PROJECT_KEY"
# Strip whitespace so "a, b" and multi-line YAML values work too.
exclusions=$(printf '%s' "$EXCLUSIONS" | tr -d '[:space:]')
if [ -n "$exclusions" ]; then
args="$args -Dsonar.exclusions=$exclusions"
fi
if [ "$QUALITY_GATE" = "true" ]; then
args="$args -Dsonar.qualitygate.wait=true"
fi
if [ -n "$EXTRA_ARGS" ]; then
args="$args $EXTRA_ARGS"
fi
echo "args=$args" >> "$GITHUB_OUTPUT"

- uses: SonarSource/sonarqube-scan-action@4d1dc0c91ee400dcf8a6dddf2f705ac36542e304 # v8.4.0
if: steps.gate.outputs.run == 'true'
with:
projectBaseDir: ${{ inputs.working-directory }}
args: ${{ steps.args.outputs.args }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- `sonarqube.yml`: new reusable workflow. SonarQube Cloud analysis that fails on a failed quality gate, for public repositories, on the default branch and pull requests into it. Private repositories opt in with the Actions variable `SONAR_SCAN_PRIVATE_REPO` set to `true`. Private repositories without it, other branches, merge-queue runs, and pull requests without the `SONAR_TOKEN` secret (forks, Dependabot) succeed without analysing. Paths to leave out go in the `exclusions` input (comma-separated globs).

- `templates/dependabot.yml`: security updates come as one grouped PR per ecosystem (`applies-to: security-updates`) instead of one PR per package. The version-update groups never applied to them.
- Caller templates: a new push to a pull request cancels that pull request's previous Status checks run (workflow-level `concurrency`). Push, schedule and merge-queue runs are never cancelled.
- Templates: no `codeql` job. Public repositories in the organization get CodeQL through default setup, which rejects results from `codeql.yml`.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Reusable GitHub Actions workflows for the [privacybydesign](https://github.com/p
| `terraform.yml` | fmt, validate, tflint, Checkov, plan and apply |
| `codeql.yml` | CodeQL for Go, JavaScript/TypeScript and workflow files, for repositories without CodeQL default setup |
| `dependency-review.yml` | Blocks pull requests that add vulnerable or disallowed-licence dependencies |
| `sonarqube.yml` | SonarQube Cloud analysis with the quality gate as a check, on public repositories |
| `workflow-lint.yml` | actionlint and zizmor |
| `pr-title.yml` | Checks that pull request titles follow Conventional Commits |
| `release-please.yml` | Release pull requests and GitHub releases |
Expand Down Expand Up @@ -48,6 +49,8 @@ The inputs of each workflow are documented in its file, under `on.workflow_call.

On private repositories, artifact attestations are skipped, and `codeql.yml` and `dependency-review.yml` need GitHub Code Security.

The Status checks templates include a `sonarqube` job. On a public repository it needs the `SONAR_TOKEN` secret, the repository imported in the privacybydesign organization on [SonarQube Cloud](https://sonarcloud.io), and Automatic Analysis turned off there. Don't forget to add the repository to `Selected repositories` of the SonarQube Cloud GitHub App (organization Settings > GitHub Apps > SonarQube Cloud > Configure), or SonarQube Cloud can't import it. The job then fails when the quality gate fails. It only analyses the default branch and pull requests into the default branch; runs on other branches succeed without analysing. On a private repository it succeeds without analysing, so you can keep or remove it, unless you set the Actions variable `SONAR_SCAN_PRIVATE_REPO` to `true` in the repository: then it analyses the private repository too (this needs a paid SonarQube Cloud plan). Our subscription allows 50k lines of code for private repositories, so check the remaining allowance in SonarQube Cloud before you opt in. Pull requests without access to the secret (forks, Dependabot without a Dependabot secret) skip the analysis. To leave paths out of the analysis without editing the template, set the Actions variable `SONAR_EXCLUSIONS` in the repository to comma-separated globs (for example `**/testdata/**,docs/**`); it replaces the template default, which every run uses otherwise, scheduled runs included.

In the privacybydesign organization, public repositories get CodeQL through GitHub's default setup (security configuration "GitHub recommended"), so the templates leave out the `codeql` job: with default setup enabled, GitHub rejects results from a separate CodeQL workflow.

## Versioning
Expand Down
18 changes: 17 additions & 1 deletion templates/workflows/status-checks-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,28 @@ jobs:
permissions:
contents: read

# SonarQube Cloud quality gate. Public repositories only: on a private
# repository it succeeds without analysing. Needs the SONAR_TOKEN secret.
sonarqube:
uses: privacybydesign/github-workflows/.github/workflows/sonarqube.yml@v1
permissions:
contents: read
pull-requests: read
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
# Comma-separated paths SonarQube does not analyse (globs, relative to the repository root).
# Set them per repository with the SONAR_EXCLUSIONS variable (Settings > Secrets and
# variables > Actions > Variables) instead of editing this file. Without it,
# every run, scheduled runs included, uses the default at the end of this line.
exclusions: ${{ vars.SONAR_EXCLUSIONS || '**/testdata/**' }}

# Single required check for rulesets/branch protection: job names inside this
# workflow can change without touching the ruleset.
passed:
name: Status checks passed
if: always()
needs: [go, docker, dependency-review, workflow-lint]
needs: [go, docker, dependency-review, workflow-lint, sonarqube]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
Expand Down
20 changes: 19 additions & 1 deletion templates/workflows/status-checks-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,30 @@ jobs:
permissions:
contents: read

# SonarQube Cloud quality gate. Public repositories only: on a private
# repository it succeeds without analysing. Needs the SONAR_TOKEN secret.
sonarqube:
uses: privacybydesign/github-workflows/.github/workflows/sonarqube.yml@v1
permissions:
contents: read
pull-requests: read
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
# Comma-separated paths SonarQube does not analyse (globs, relative to the repository root).
# Set them per repository with the SONAR_EXCLUSIONS variable (Settings > Secrets and
# variables > Actions > Variables) instead of editing this file. Without it,
# every run, scheduled runs included, uses the default at the end of this line.
# Example: "**/generated/**,public/vendor/**"
exclusions: ${{ vars.SONAR_EXCLUSIONS || '' }}
# args: -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info

# Single required check for rulesets/branch protection: job names inside this
# workflow can change without touching the ruleset.
passed:
name: Status checks passed
if: always()
needs: [node, docker, dependency-review, workflow-lint]
needs: [node, docker, dependency-review, workflow-lint, sonarqube]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
Expand Down
Loading