mirror of
https://github.com/SonarSource/sonarqube-scan-action.git
synced 2026-04-30 09:28:44 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59db25f34e | |||
| ca30b65f4e | |||
| c7ee0f9df9 | |||
| 55e44800a8 | |||
| 30dbe5c9ee | |||
| c8357220fa | |||
| f00de44f57 | |||
| f099b44166 | |||
| d899ed2996 | |||
| 299e4b793a | |||
| 3988e54db2 | |||
| 9598b8a83f | |||
| dcc5211de5 | |||
| b9f37f9de0 | |||
| a31c9398be | |||
| 40f5b61913 | |||
| 9bf7c126a1 | |||
| ba6563cca7 | |||
| 5ffbad4454 | |||
| fd88b7d7cc | |||
| 27a157d234 | |||
| e327da8e78 | |||
| ff001fd600 | |||
| a88c96d7e4 | |||
| a64281002c | |||
| 60aee7033b | |||
| 502204eab4 | |||
| 0b794a06fa | |||
| ece10df5d7 | |||
| ee80e84272 | |||
| cbabf0572a | |||
| 16df975da5 | |||
| ed9f3aad50 | |||
| 8f448484d9 | |||
| 6a808e9a20 | |||
| 9db61695c9 | |||
| 5837ebfcca |
+1
-1
@@ -1 +1 @@
|
||||
.github/* @sonarsource/orchestration-processing-squad
|
||||
.github/* @sonarsource/quality-processing-squad
|
||||
|
||||
@@ -12,5 +12,9 @@ updates:
|
||||
interval: "daily"
|
||||
timezone: "CET"
|
||||
open-pull-requests-limit: 100
|
||||
cooldown:
|
||||
default-days: 5
|
||||
exclude:
|
||||
- "SonarSource/*"
|
||||
commit-message:
|
||||
prefix: "NO-JIRA "
|
||||
|
||||
@@ -5,9 +5,11 @@ services:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- $GITHUB_WORKSPACE/.github/qa-nginx-redirecting/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- $GITHUB_WORKSPACE/.github/qa-nginx-redirecting/nginx.crt:/etc/nginx/nginx.crt:ro
|
||||
- $GITHUB_WORKSPACE/.github/qa-nginx-redirecting/nginx.key:/etc/nginx/nginx.key:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--fail", "localhost:8080/health"]
|
||||
test: ["CMD", "curl", "--fail", "--insecure", "https://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 2m
|
||||
start_period: 2m
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate self-signed SSL certificate for localhost with 1-day expiry
|
||||
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
|
||||
-keyout nginx.key \
|
||||
-out nginx.crt \
|
||||
-subj "/C=US/ST=CA/L=Local/O=Test/CN=localhost" \
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
|
||||
echo "SSL certificates generated with 1-day expiry: nginx.crt and nginx.key"
|
||||
@@ -18,7 +18,9 @@ http {
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
listen 8080 ssl;
|
||||
ssl_certificate /etc/nginx/nginx.crt;
|
||||
ssl_certificate_key /etc/nginx/nginx.key;
|
||||
|
||||
location /health {
|
||||
add_header 'Content-Type' 'text/plain';
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
PullRequestClosed_job:
|
||||
name: Pull Request Closed
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: read
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
PullRequestCreated_job:
|
||||
name: Pull Request Created
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
# For external PR, ticket should be created manually
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
RequestReview_job:
|
||||
name: Request review
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
# For external PR, ticket should be moved manually
|
||||
|
||||
@@ -7,10 +7,9 @@ on:
|
||||
jobs:
|
||||
SubmitReview_job:
|
||||
name: Submit Review
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: read
|
||||
# For external PR, ticket should be moved manually
|
||||
if: |
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
@@ -21,10 +20,11 @@ jobs:
|
||||
uses: SonarSource/vault-action-wrapper@v3
|
||||
with:
|
||||
secrets: |
|
||||
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN;
|
||||
development/kv/data/jira user | JIRA_USER;
|
||||
development/kv/data/jira token | JIRA_TOKEN;
|
||||
- uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }}
|
||||
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
|
||||
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
|
||||
|
||||
@@ -11,13 +11,16 @@ jobs:
|
||||
output-test:
|
||||
name: Action outputs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest-large, windows-latest-large, macos-latest, macos-13]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest, macos-14]
|
||||
cache: [true, false]
|
||||
include:
|
||||
- arch: X64
|
||||
- os: macos-latest
|
||||
arch: ARM64
|
||||
- os: macos-14
|
||||
arch: ARM64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# Specifying a specific architecture of the runner is not possible for Github hosted runners
|
||||
@@ -31,7 +34,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
|
||||
@@ -11,13 +11,16 @@ jobs:
|
||||
output-test:
|
||||
name: Action outputs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest-large, windows-latest-large, macos-latest, macos-13]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest, macos-14]
|
||||
cache: [true, false]
|
||||
include:
|
||||
- arch: X64
|
||||
- os: macos-latest
|
||||
arch: ARM64
|
||||
- os: macos-14
|
||||
arch: ARM64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# Specifying a specific architecture of the runner is not possible for Github hosted runners
|
||||
@@ -31,7 +34,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
|
||||
+83
-178
@@ -12,11 +12,12 @@ jobs:
|
||||
name: >
|
||||
No inputs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action without args
|
||||
@@ -31,11 +32,12 @@ jobs:
|
||||
name: >
|
||||
'args' input
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with args
|
||||
@@ -54,15 +56,21 @@ jobs:
|
||||
name: >
|
||||
'args' input with command injection will fail
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
args: [ -Dsonar.someArg=aValue && echo "Injection", -Dsonar.someArg="value\"; whoami; echo \"" ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
args:
|
||||
[
|
||||
-Dsonar.someArg=aValue && echo "Injection",
|
||||
-Dsonar.someArg="value\"; whoami; echo \"",
|
||||
]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with args
|
||||
id: runTest
|
||||
uses: ./
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -80,11 +88,12 @@ jobs:
|
||||
name: >
|
||||
'args' input with backticks injection does not execute command
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with args
|
||||
@@ -109,10 +118,10 @@ jobs:
|
||||
'args' input with dollar command injection does not execute command
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with args
|
||||
@@ -136,10 +145,10 @@ jobs:
|
||||
'args' input with other command injection variants does not execute command
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with args
|
||||
@@ -166,10 +175,10 @@ jobs:
|
||||
'projectBaseDir' input
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: mkdir -p ./baseDir
|
||||
@@ -187,9 +196,9 @@ jobs:
|
||||
scannerVersionTest:
|
||||
name: >
|
||||
'scannerVersion' input
|
||||
runs-on: ubuntu-latest-large # assumes default RUNNER_ARCH for linux is X64
|
||||
runs-on: github-ubuntu-latest-s # assumes default RUNNER_ARCH for linux is X64
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with scannerVersion
|
||||
@@ -203,13 +212,17 @@ jobs:
|
||||
SONAR_SCANNER_JSON_PARAMS: '{"sonar.scanner.internal.dumpToFile": "./output.properties"}'
|
||||
- name: Assert
|
||||
run: |
|
||||
./test/assertFileExists "$RUNNER_TEMP/sonarscanner/sonar-scanner-cli-6.1.0.4477-linux-x64.zip"
|
||||
# Verify the tool was installed by checking it's in PATH
|
||||
if ! command -v sonar-scanner &> /dev/null; then
|
||||
echo "Error: sonar-scanner not found in PATH"
|
||||
exit 1
|
||||
fi
|
||||
scannerBinariesUrlTest:
|
||||
name: >
|
||||
'scannerBinariesUrl' input with invalid URL
|
||||
runs-on: ubuntu-latest-large # assumes default RUNNER_ARCH for linux is X64
|
||||
runs-on: github-ubuntu-latest-s # assumes default RUNNER_ARCH for linux is X64
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with scannerBinariesUrl
|
||||
@@ -235,9 +248,9 @@ jobs:
|
||||
scannerBinariesUrlIsEscapedWithWget:
|
||||
name: >
|
||||
'scannerBinariesUrl' is escaped with wget so special chars are not injected in the download command
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with scannerBinariesUrl
|
||||
@@ -245,7 +258,7 @@ jobs:
|
||||
uses: ./
|
||||
continue-on-error: true
|
||||
with:
|
||||
scannerBinariesUrl: 'http://some_uri;touch file.txt;'
|
||||
scannerBinariesUrl: "http://some_uri;touch file.txt;"
|
||||
env:
|
||||
NO_CACHE: true
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
@@ -256,9 +269,9 @@ jobs:
|
||||
scannerBinariesUrlIsEscapedWithCurl:
|
||||
name: >
|
||||
'scannerBinariesUrl' is escaped with curl so special chars are not injected in the download command
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove wget
|
||||
@@ -274,7 +287,7 @@ jobs:
|
||||
uses: ./
|
||||
continue-on-error: true
|
||||
with:
|
||||
scannerBinariesUrl: 'http://some_uri http://another_uri''; touch file.txt;'
|
||||
scannerBinariesUrl: "http://some_uri http://another_uri'; touch file.txt;"
|
||||
env:
|
||||
NO_CACHE: true
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
@@ -285,9 +298,9 @@ jobs:
|
||||
dontFailGradleTest:
|
||||
name: >
|
||||
Don't fail on Gradle project
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action on Gradle project
|
||||
@@ -306,9 +319,9 @@ jobs:
|
||||
dontFailGradleKotlinTest:
|
||||
name: >
|
||||
Don't fail on Kotlin Gradle project
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action on Kotlin Gradle project
|
||||
@@ -327,9 +340,9 @@ jobs:
|
||||
dontFailMavenTest:
|
||||
name: >
|
||||
Don't fail on Maven project
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action on Maven project
|
||||
@@ -346,7 +359,7 @@ jobs:
|
||||
run: |
|
||||
./test/assertFileExists ./output.properties
|
||||
runAnalysisTest:
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
services:
|
||||
sonarqube:
|
||||
image: sonarqube:lts-community
|
||||
@@ -362,7 +375,7 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action on sample project
|
||||
@@ -380,11 +393,12 @@ jobs:
|
||||
name: >
|
||||
'RUNNER_DEBUG' is used
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with debug mode
|
||||
@@ -399,7 +413,7 @@ jobs:
|
||||
run: |
|
||||
./test/assertFileContains ./output.properties "sonar.verbose=true"
|
||||
runAnalysisWithCacheTest:
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
services:
|
||||
sonarqube:
|
||||
image: sonarqube:lts-community
|
||||
@@ -415,11 +429,11 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: SonarQube Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sonar/cache
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-sonar
|
||||
@@ -434,16 +448,17 @@ jobs:
|
||||
projectBaseDir: ./test/example-project
|
||||
- name: Assert
|
||||
run: |
|
||||
./test/assertFileExists ./test/example-project/.scannerwork/report-task.txt
|
||||
./test/assertFileExists ./test/example-project/.scannerwork/report-task.txt
|
||||
overrideSonarcloudUrlTest:
|
||||
name: >
|
||||
'SONARCLOUD_URL' is used
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with SONARCLOUD_URL
|
||||
@@ -456,98 +471,13 @@ jobs:
|
||||
- name: Assert
|
||||
run: |
|
||||
./test/assertFileContains ./output.properties "sonar.host.url=mirror.sonarcloud.io"
|
||||
./test/assertFileContains ./output.properties "sonar.scanner.sonarcloudUrl=mirror.sonarcloud.io"
|
||||
dontFailWhenMissingWgetButCurlAvailable:
|
||||
name: Don't fail when missing wget but curl available
|
||||
runs-on: ubuntu-latest-large
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove wget
|
||||
run: sudo apt-get remove -y wget
|
||||
- name: Assert wget is not available
|
||||
run: |
|
||||
if command -v wget 2>&1 >/dev/null
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
- name: Run action
|
||||
uses: ./
|
||||
env:
|
||||
NO_CACHE: true
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_SCANNER_JSON_PARAMS: '{"sonar.scanner.internal.dumpToFile": "./output.properties"}'
|
||||
with:
|
||||
args: -Dsonar.scanner.internal.dumpToFile=./output.properties
|
||||
- name: Assert
|
||||
run: |
|
||||
./test/assertFileExists ./output.properties
|
||||
dontFailWhenMissingCurlButWgetAvailable:
|
||||
name: Don't fail when missing curl but wget available
|
||||
runs-on: ubuntu-latest-large
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove curl
|
||||
run: sudo apt-get remove -y curl
|
||||
- name: Assert curl is not available
|
||||
run: |
|
||||
if command -v curl 2>&1 >/dev/null
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
- name: Run action
|
||||
id: runTest
|
||||
uses: ./
|
||||
env:
|
||||
NO_CACHE: true
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_SCANNER_JSON_PARAMS: '{"sonar.scanner.internal.dumpToFile": "./output.properties"}'
|
||||
with:
|
||||
args: -Dsonar.scanner.internal.dumpToFile=./output.properties
|
||||
- name: Assert
|
||||
run: |
|
||||
./test/assertFileExists ./output.properties
|
||||
failWhenBothWgetAndCurlMissing:
|
||||
name: Fail when both wget and curl are missing
|
||||
runs-on: ubuntu-latest-large
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove wget and curl
|
||||
run: sudo apt-get remove -y wget curl
|
||||
- name: Assert wget and curl are not available
|
||||
run: |
|
||||
if command -v wget 2>&1 >/dev/null
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
if command -v curl 2>&1 >/dev/null
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
- name: Run action
|
||||
id: runTest
|
||||
uses: ./
|
||||
continue-on-error: true
|
||||
env:
|
||||
NO_CACHE: true
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_SCANNER_JSON_PARAMS: '{"sonar.scanner.internal.dumpToFile": "./output.properties"}'
|
||||
with:
|
||||
args: -Dsonar.scanner.internal.dumpToFile=./output.properties
|
||||
- name: Assert failure of previous step
|
||||
if: steps.runTest.outcome == 'success'
|
||||
run: exit 1
|
||||
./test/assertFileContains ./output.properties "sonar.scanner.sonarcloudUrl=mirror.sonarcloud.io"
|
||||
curlPerformsRedirect:
|
||||
name: >
|
||||
curl performs redirect when scannerBinariesUrl returns 3xx
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove wget
|
||||
@@ -558,6 +488,9 @@ jobs:
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
- name: Generate SSL certificates for nginx
|
||||
run: ./generate-ssl.sh
|
||||
working-directory: .github/qa-nginx-redirecting
|
||||
- name: Start nginx via Docker Compose
|
||||
run: docker compose up -d --wait
|
||||
working-directory: .github/qa-nginx-redirecting
|
||||
@@ -566,23 +499,29 @@ jobs:
|
||||
uses: ./
|
||||
with:
|
||||
scannerVersion: 6.2.1.4610
|
||||
scannerBinariesUrl: http://localhost:8080/clientRedirectToSonarBinaries
|
||||
scannerBinariesUrl: https://localhost:8080/clientRedirectToSonarBinaries
|
||||
env:
|
||||
NO_CACHE: true
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_SCANNER_JSON_PARAMS: '{"sonar.scanner.internal.dumpToFile": "./output1.properties"}'
|
||||
- name: Assert Sonar Scanner CLI was downloaded
|
||||
run: |
|
||||
./test/assertFileExists "$RUNNER_TEMP/sonarscanner/sonar-scanner-cli-6.2.1.4610-linux-x64.zip"
|
||||
# Verify the tool was installed by checking it's in PATH
|
||||
if ! command -v sonar-scanner &> /dev/null; then
|
||||
echo "Error: sonar-scanner not found in PATH"
|
||||
exit 1
|
||||
fi
|
||||
useSslCertificate:
|
||||
name: >
|
||||
'SONAR_ROOT_CERT' is converted to truststore
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest-large, windows-latest-large, macos-latest ]
|
||||
os: [github-ubuntu-latest-s, github-windows-latest-s, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with SSL certificate
|
||||
@@ -631,9 +570,9 @@ jobs:
|
||||
analysisWithSslCertificate:
|
||||
name: >
|
||||
Analysis takes into account 'SONAR_ROOT_CERT'
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate server certificate
|
||||
@@ -652,7 +591,7 @@ jobs:
|
||||
- name: Start nginx and SonarQube via Docker Compose
|
||||
run: docker compose up -d --wait
|
||||
working-directory: .github/qa-sq-behind-ngix
|
||||
- name: Read correct server certificate
|
||||
- name: Read correct server certificate
|
||||
run: |
|
||||
# read server.crt from .github/qa-sq-behind-ngix/ and store into the SONAR_ROOT_CERT_VALID
|
||||
# environment variable, to be able to read it in the next step
|
||||
@@ -736,46 +675,12 @@ jobs:
|
||||
- name: Assert failure of previous step
|
||||
if: steps.wrong_ssl_certificate.outcome == 'success'
|
||||
run: exit 1
|
||||
overridesScannerLocalFolderWhenPresent: # can happen in uncleaned self-hosted runners
|
||||
name: >
|
||||
'SCANNER_LOCAL_FOLDER' is cleaned with warning when present
|
||||
runs-on: ubuntu-latest-large
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create a dummy SCANNER_LOCAL_FOLDER with dummy content in it
|
||||
run: |
|
||||
SCANNER_VERSION="6.2.1.4610"
|
||||
SCANNER_LOCAL_FOLDER="$RUNNER_TEMP/sonar-scanner-cli-$SCANNER_VERSION-$RUNNER_OS-$RUNNER_ARCH"
|
||||
# emit SCANNER_VERSION and SCANNER_LOCAL_FOLDER to be able to read them in the next steps
|
||||
echo "SCANNER_VERSION=$SCANNER_VERSION" >> $GITHUB_ENV
|
||||
echo "SCANNER_LOCAL_FOLDER=$SCANNER_LOCAL_FOLDER" >> $GITHUB_ENV
|
||||
mkdir -p "$SCANNER_LOCAL_FOLDER"
|
||||
touch "$SCANNER_LOCAL_FOLDER/some_content.txt"
|
||||
- name: Assert SCANNER_LOCAL_FOLDER exists and dummy file is in it
|
||||
run: |
|
||||
[ -d "$SCANNER_LOCAL_FOLDER" ] || exit 1
|
||||
[ -f "$SCANNER_LOCAL_FOLDER/some_content.txt" ] || exit 1
|
||||
- name: Run action with SONAR_SCANNER_TEMP
|
||||
uses: ./
|
||||
env:
|
||||
NO_CACHE: true # force install-sonar-scanner-cli.sh execution
|
||||
SONAR_SCANNER_TEMP: /tmp/sonar-scanner
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
with:
|
||||
args: -Dsonar.scanner.internal.dumpToFile=./output.properties
|
||||
scannerVersion: ${{ env.SCANNER_VERSION }}
|
||||
- name: Assert SCANNER_LOCAL_FOLDER exists and dummy file is not in it
|
||||
run: |
|
||||
[ -d "$SCANNER_LOCAL_FOLDER" ] || exit 1
|
||||
[ ! -f "$SCANNER_LOCAL_FOLDER/some_content.txt" ] || exit 1
|
||||
updateTruststoreWhenPresent: # can happen in uncleaned self-hosted runners
|
||||
name: >
|
||||
truststore.p12 is updated when present
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create SONAR_SSL_FOLDER with a file in it (not-truststore.p12)
|
||||
@@ -791,7 +696,7 @@ jobs:
|
||||
- name: Run action with SONAR_ROOT_CERT
|
||||
uses: ./
|
||||
env:
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_ROOT_CERT: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
@@ -840,7 +745,7 @@ jobs:
|
||||
- name: Run action a second time with a different SONAR_ROOT_CERT
|
||||
uses: ./
|
||||
env:
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_ROOT_CERT: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
@@ -873,7 +778,7 @@ jobs:
|
||||
- name: Run action a third time
|
||||
uses: ./
|
||||
env:
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
# NO_CACHE not needed, as SONAR_SSL_FOLDER is setup when the Sonar Scanner is run, not installed
|
||||
SONAR_HOST_URL: http://not_actually_used
|
||||
SONAR_ROOT_CERT: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
@@ -902,9 +807,9 @@ jobs:
|
||||
scannerVersionValidationTest:
|
||||
name: >
|
||||
'scannerVersion' input validation
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run action with invalid scannerVersion
|
||||
|
||||
@@ -10,9 +10,9 @@ on:
|
||||
jobs:
|
||||
create-install-dir-test:
|
||||
name: create_install_path.sh
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
grep "=== Script failed ===" output
|
||||
setup-script-test:
|
||||
name: configure_paths.sh
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
env:
|
||||
INSTALL_PATH: 'install-directory'
|
||||
SONAR_HOST_URL: 'http://sonar-host.com'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
SONAR_SCANNER_URL_MACOSX_AARCH64: 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-vX.Y.Z.MMMM-macosx-aarch64.zip'
|
||||
SONAR_SCANNER_SHA_MACOSX_AARCH64: 'DOWNLOAD-SHA-MACOSX-AARCH64'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
@@ -250,9 +250,9 @@ jobs:
|
||||
grep "=== Script failed ===" output
|
||||
download-script-test:
|
||||
name: download.sh
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
@@ -319,9 +319,9 @@ jobs:
|
||||
grep "=== Script failed ===" output
|
||||
fetch-latest-version-test:
|
||||
name: fetch_latest_version.sh
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Test script
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Unit tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Configure NPM with Repox
|
||||
uses: SonarSource/ci-github-actions/config-npm@v1
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Check dist/ is up-to-date
|
||||
run: |
|
||||
if ! git diff --exit-code dist/ || [ -n "$(git status --porcelain dist/)" ]; then
|
||||
echo "::error::dist/ is out of date. Run 'npm run build' and commit the changes."
|
||||
git status --short dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
@@ -7,13 +7,13 @@ on:
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Parse semver
|
||||
uses: madhead/semver-utils@36d1e0ed361bd7b4b77665de8093092eaeabe6ba # v4.3.0
|
||||
|
||||
@@ -7,13 +7,13 @@ on:
|
||||
jobs:
|
||||
check-version:
|
||||
name: Check for sonar-scanner version update
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
outputs:
|
||||
should_update: ${{ steps.version-check.outputs.should_update }}
|
||||
new-version: ${{ steps.latest-version.outputs.sonar-scanner-version }}
|
||||
steps:
|
||||
- run: sudo apt install -y jq
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
update-version:
|
||||
name: Prepare pull request for sonar-scanner version update
|
||||
needs: check-version
|
||||
runs-on: ubuntu-latest-large
|
||||
runs-on: github-ubuntu-latest-s
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: needs.check-version.outputs.should_update == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: true
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Path to sources
|
||||
sonar.sources=src
|
||||
sonar.exclusions=src/**/__tests__/*
|
||||
# sonar.inclusions=
|
||||
|
||||
# Path to tests
|
||||
sonar.tests=test,src
|
||||
# sonar.test.exclusions=
|
||||
sonar.test.inclusions=src/**/__tests__/*
|
||||
|
||||
# Source encoding
|
||||
# sonar.sourceEncoding=
|
||||
|
||||
# Exclusions for copy-paste detection
|
||||
# sonar.cpd.exclusions=
|
||||
|
||||
# Python version (for python projects only)
|
||||
# sonar.python.version=
|
||||
|
||||
# C++ standard version (for C++ projects only)
|
||||
# If not specified, it defaults to the latest supported standard
|
||||
# sonar.cfamily.reportingCppStandardOverride=c++98|c++11|c++14|c++17|c++20
|
||||
@@ -1,53 +1,42 @@
|
||||
# Scan your code with SonarQube [](https://github.com/SonarSource/sonarqube-scan-action/actions/workflows/qa-main.yml) [](https://github.com/SonarSource/sonarqube-scan-action/actions/workflows/qa-install-build-wrapper.yml) [](https://github.com/SonarSource/sonarqube-scan-action/actions/workflows/qa-scripts.yml) [](https://github.com/SonarSource/sonarqube-scan-action/actions/workflows/qa-deprecated-c-cpp.yml)
|
||||
|
||||
This SonarSource project, available as a GitHub Action, scans your projects with SonarQube [Server](https://www.sonarsource.com/products/sonarqube/) or [Cloud](https://www.sonarsource.com/products/sonarcloud/).
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./images/SQ_Logo_Server_Cloud_Dark_Backgrounds.png">
|
||||
<img alt="SonarQube Logo" src="./images/SQ_Logo_Server_Cloud_Light_Backgrounds.png">
|
||||
</picture>
|
||||
|
||||
SonarQube [Server](https://www.sonarsource.com/products/sonarqube/) and [Cloud](https://www.sonarsource.com/products/sonarcloud/) (formerly SonarQube and SonarCloud) is a widely used static analysis solution for continuous code quality and security inspection.
|
||||
|
||||
It helps developers detect coding issues in 30+ languages, frameworks, and IaC platforms, including Java, JavaScript, TypeScript, C#, Python, C, C++, and [many more](https://www.sonarsource.com/knowledge/languages/).
|
||||
## A GitHub Action for SonarQube
|
||||
This GitHub Action integrates continuous code quality and security analysis directly into your workflow. It scans your project with either [SonarQube Server](https://www.sonarsource.com/products/sonarqube/) or [SonarQube Cloud](https://www.sonarsource.com/products/sonarcloud/), helping you catch bugs, security vulnerabilities, and code smells automatically within your CI/CD pipeline. **This action is the official method for scanning C, C++, Objective-C, and Dart projects via GitHub Actions.**
|
||||
|
||||
The solution also provides fix recommendations leveraging AI with Sonar's AI CodeFix capability.
|
||||
|
||||
> [!NOTE]
|
||||
> This action now supports and is the official entrypoint for scanning C, C++, Objective-C and Dart projects via GitHub actions.
|
||||
### What is SonarQube?
|
||||
[SonarQube Server](https://www.sonarsource.com/products/sonarqube/) and [SonarQube Cloud](https://www.sonarsource.com/products/sonarcloud/) are widely used static analysis solutions for continuous code quality, security inspection, and fix remediation.
|
||||
The platform supports over in 30+ languages, frameworks, and IaC platforms, including Java, JavaScript, TypeScript, C#, Python, C, C++, and [many more](https://www.sonarsource.com/knowledge/languages/).
|
||||
|
||||
## Requirements
|
||||
## Quick Start
|
||||
|
||||
### Server
|
||||
### 1. Prerequisites:
|
||||
|
||||
To run an analysis on your code, you first need to set up your project on SonarQube Server. Your SonarQube Server instance must be accessible from GitHub, and you will need an access token to run the analysis (more information below under **Environment variables**).
|
||||
You must have a project already set up on SonarQube Cloud or SonarQube Server. This action performs the analysis, but the project must exist on the platform to receive the results.
|
||||
|
||||
Read more information on how to analyze your code [here](https://docs.sonarsource.com/sonarqube-server/latest/devops-platform-integration/github-integration/introduction/).
|
||||
For more information, see [Key Requirements](#key-requirements).
|
||||
|
||||
### Cloud
|
||||
|
||||
* Create your account on SonarQube Cloud. [Sign up for free](https://www.sonarsource.com/products/sonarcloud/signup/?utm_medium=referral&utm_source=github&utm_campaign=sc-signup&utm_content=signup-sonarcloud-listing-x-x&utm_term=ww-psp-x) now if it's not already the case!
|
||||
* The repository to analyze is set up on SonarQube Cloud. [Set it up](https://sonarcloud.io/projects/create) in just one click.
|
||||
### 2. Required variables:
|
||||
|
||||
## Usage
|
||||
The action needs two key variables to connect to the SonarQube instance and run the analysis. These should be stored as GitHub secrets or variables for security.
|
||||
|
||||
Project metadata, including the location of the sources to be analyzed, must be declared in the file `sonar-project.properties` in the base directory:
|
||||
• `SONAR_TOKEN` : The authentication token required to access the SonarQube instance. This is a mandatory secret for all use cases.
|
||||
|
||||
### Server
|
||||
• `SONAR_HOST_URL` : The URL of the SonarQube Server. This is required for self-hosted SonarQube Server but not needed for SonarQube Cloud.
|
||||
|
||||
```properties
|
||||
sonar.projectKey=<replace with the key generated when setting up the project on SonarQube Server>
|
||||
For more information, see [Configuration](#configuration).
|
||||
|
||||
# relative paths to source directories. More details and properties are described
|
||||
# at https://docs.sonarsource.com/sonarqube-server/latest/project-administration/analysis-scope/
|
||||
sonar.sources=.
|
||||
```
|
||||
### 3. Quick Start Workflow Example (for SonarQube Cloud)
|
||||
|
||||
In the following cases:
|
||||
- for projects that don't have C, C++, or Objective-C in them
|
||||
- for C, C++, Objective-C projects that don't use [Build Wrapper](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper)
|
||||
Create or update your CI pipeline to run the scan action:
|
||||
|
||||
the workflow, usually declared under `.github/workflows`, looks like the following:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
@@ -66,76 +55,18 @@ jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Disabling shallow clones is recommended for improving the relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version> # Ex: v4.1.0, See the latest version at https://github.com/marketplace/actions/official-sonarqube-scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version or sha1> # Ex: v4.1.0 or sha1, See the latest version at https://github.com/marketplace/actions/official-sonarqube-scan
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
```
|
||||
|
||||
For C, C++, and Objective-C projects relying on [Build Wrapper](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper) to generate the compilation database, the workflow requires additional steps to download the Build Wrapper and invoke it:
|
||||
Create a configuration file in the root directory of the project and name it `sonar-project.properties`:
|
||||
|
||||
```yaml
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Disabling shallow clone is recommended for improving relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: Install Build Wrapper
|
||||
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@<action version>
|
||||
env:
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
- name: Run Build Wrapper
|
||||
run: |
|
||||
# Here goes your compilation wrapped with Build Wrapper
|
||||
# For more information, see https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper
|
||||
# build-preparation steps
|
||||
# build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} build-command
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version>
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
SONAR_ROOT_CERT: ${{ secrets.SONAR_ROOT_CERT }}
|
||||
with:
|
||||
# Consult https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner/ for more information and options
|
||||
args: >
|
||||
--define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||
```
|
||||
|
||||
If you are using SonarQube Server 10.5 or earlier, use `sonar.cfamily.build-wrapper-output` instead of `sonar.cfamily.compile-commands` in the `args` property of the last step, as Build Wrapper does not generate a `compile_commands.json` file before SonarQube Server 10.6.
|
||||
|
||||
It should look like this:
|
||||
|
||||
```yaml
|
||||
with:
|
||||
args: >
|
||||
--define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
||||
```
|
||||
|
||||
See also [example configurations of C++ projects for SonarQube Server](https://github.com/search?q=org%3Asonarsource-cfamily-examples+gh-actions-sq&type=repositories).
|
||||
|
||||
### Cloud
|
||||
|
||||
```properties
|
||||
sonar.organization=<replace with your SonarQube Cloud organization key>
|
||||
@@ -143,105 +74,75 @@ sonar.projectKey=<replace with the key generated when setting up the project on
|
||||
|
||||
# relative paths to source directories. More details and properties are described
|
||||
# at https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/analysis-scope/
|
||||
sonar.sources=.
|
||||
sonar.sources=src
|
||||
```
|
||||
|
||||
In the following cases:
|
||||
- for projects that don't have C, C++, or Objective-C in them
|
||||
- for C, C++, Objective-C projects that don't use [Build Wrapper](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper)
|
||||
For other workflows, see [Workflow Examples](#workflow-examples).
|
||||
|
||||
the workflow, usually declared under `.github/workflows`, looks like the following:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Disabling shallow clones is recommended for improving the relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version> # Ex: v4.1.0, See the latest version at https://github.com/marketplace/actions/official-sonarqube-scan
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
```
|
||||
## Important: Special Cases and alternatives
|
||||
|
||||
For C, C++, and Objective-C projects relying on [Build Wrapper](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper) to generate the compilation database, the workflow requires additional steps to download the Build Wrapper and invoke it:
|
||||
This GitHub Action will not work for all technologies. If you are in one of the following situations, you should use the following alternatives:
|
||||
|
||||
```yaml
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
* **Your code is built with Maven**. Read the documentation about our SonarScanner for Maven in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner-for-maven/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-maven/).
|
||||
* **Your code is built with Gradle**. Read the documentation about our SonarScanner for Gradle in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-gradle/).
|
||||
* **You want to analyze a .NET solution**. Read the documentation about our SonarScanner for .NET in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/dotnet/introduction/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-dotnet/introduction/).
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Disabling shallow clone is recommended for improving relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: Install Build Wrapper
|
||||
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@<action version>
|
||||
- name: Run Build Wrapper
|
||||
run: |
|
||||
# Here goes your compilation wrapped with Build Wrapper
|
||||
# For more information, see https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper
|
||||
# build-preparation steps
|
||||
# build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} build-command
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version>
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_ROOT_CERT: ${{ secrets.SONAR_ROOT_CERT }}
|
||||
with:
|
||||
# Consult https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner/ for more information and options
|
||||
args: >
|
||||
--define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||
```
|
||||
**Do not use this GitHub action if:**
|
||||
|
||||
See also [example configurations of C++ projects for SonarQube Cloud](https://github.com/search?q=org%3Asonarsource-cfamily-examples+gh-actions-sc&type=repositories).
|
||||
* You want to run the action on C, C++, or Objective-C projects on a 32-bits system - build wrappers support only 64-bits OS.
|
||||
|
||||
## Action parameters
|
||||
**If you want to use Software Composition Analysis (SCA)**
|
||||
|
||||
Dependency scanning with SonarQube Advanced Security SCA may not work correctly if scanning requires on-the-fly manifest file generation. See the SCA analysis environment requirement documentation for [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-security/analyzing-projects-for-dependencies-sca#appropriate-environment) or [Server](https://docs.sonarsource.com/sonarqube-server/advanced-security/analyzing-projects-for-dependencies#appropriate-environment).
|
||||
|
||||
## Key requirements
|
||||
|
||||
To use this GitHub Action you need to meet the following prerequisites for your choosen SonarQube platform.
|
||||
|
||||
### For SonarQube Cloud
|
||||
|
||||
* Create your account on SonarQube Cloud. [Sign up for free](https://www.sonarsource.com/products/sonarcloud/signup/?utm_medium=referral&utm_source=github&utm_campaign=sc-signup&utm_content=signup-sonarcloud-listing-x-x&utm_term=ww-psp-x) now if it's not already the case!
|
||||
* [Set up a repository to be analyzed](https://sonarcloud.io/projects/create) in just one click.
|
||||
|
||||
### For SonarQube Server
|
||||
|
||||
* Your SonarQube Server instance must be accessible from GitHub, and you will need an access token to run the analysis (more information below under **Environment variables**).
|
||||
* To run an analysis on your code, you first need to set up your project on SonarQube Server.
|
||||
|
||||
Read more information on how to analyze your code [here](https://docs.sonarsource.com/sonarqube-server/latest/devops-platform-integration/github-integration/introduction/).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Action parameters
|
||||
|
||||
#### `projectBaseDir`
|
||||
|
||||
You can change the analysis base directory by using the optional input `projectBaseDir` like this:
|
||||
|
||||
```yaml
|
||||
- uses: SonarSource/sonarqube-scan-action@<action version>
|
||||
- uses: SonarSource/sonarqube-scan-action@<action version or sha1>
|
||||
with:
|
||||
projectBaseDir: app/src
|
||||
```
|
||||
|
||||
#### `scannerVersion`
|
||||
|
||||
In case you need to specify the version of the Sonar Scanner, you can use the `scannerVersion` option:
|
||||
|
||||
```yaml
|
||||
- uses: SonarSource/sonarqube-scan-action@<action version>
|
||||
- uses: SonarSource/sonarqube-scan-action@<action version or sha1>
|
||||
with:
|
||||
scannerVersion: 6.2.0.4584
|
||||
```
|
||||
|
||||
#### `args`
|
||||
|
||||
In case you need to add additional analysis parameters, and you do not wish to set them in the `sonar-project.properties` file, you can use the `args` option:
|
||||
|
||||
```yaml
|
||||
@@ -250,6 +151,7 @@ In case you need to add additional analysis parameters, and you do not wish to s
|
||||
projectBaseDir: app/src
|
||||
args: >
|
||||
-Dsonar.organization=my-organization # For SonarQube Cloud only
|
||||
"-Dsonar.projectName=My Project"
|
||||
-Dsonar.projectKey=my-projectkey
|
||||
-Dsonar.python.coverage.reportPaths=coverage.xml
|
||||
-Dsonar.sources=lib/
|
||||
@@ -258,6 +160,36 @@ In case you need to add additional analysis parameters, and you do not wish to s
|
||||
-Dsonar.verbose=true
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> In version 6, the way the `args` option is handled has been changed to prevent command injection.
|
||||
> As a result, we no longer support the full bash syntax.
|
||||
> This means there is now a much more restricted use of quoting and escaping compared to older versions of the action.
|
||||
> Example:
|
||||
> ```yaml
|
||||
> with:
|
||||
> args: >
|
||||
> -testing test
|
||||
> -valid=true
|
||||
> --quotes "test quotes" "nested \'quotes\'"
|
||||
> -Dsonar.property="some value"
|
||||
> "-Dsonar.property=some value"
|
||||
> ```
|
||||
> will be parsed as the following array of strings:
|
||||
> ```
|
||||
> [
|
||||
> '-testing',
|
||||
> 'test',
|
||||
> '-valid=true',
|
||||
> '--quotes',
|
||||
> 'test quotes', # Surrounding quotes are removed
|
||||
> 'nested \'quotes\'',
|
||||
> '-Dsonar.property="some value"', # Internal quotes are NOT removed, contrary to the bash syntax
|
||||
> '-Dsonar.property=some value', # This is the proper way to pass scanner arguments with spaces
|
||||
> ]
|
||||
> ```
|
||||
|
||||
#### `scannerBinariesUrl`
|
||||
|
||||
You can also specify the URL where to retrieve the SonarScanner CLI from.
|
||||
The specified URL overrides the default address: `https://binaries.sonarsource.com/Distribution/sonar-scanner-cli`.
|
||||
This can be useful when the runner executing the action is self-hosted and has regulated or no access to the Internet:
|
||||
@@ -268,10 +200,28 @@ This can be useful when the runner executing the action is self-hosted and has r
|
||||
scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/
|
||||
```
|
||||
|
||||
#### `skipSignatureVerification`
|
||||
|
||||
By default, the action verifies the OpenPGP signature of the SonarScanner CLI binary before executing it. You can disable this verification using the `skipSignatureVerification` option:
|
||||
|
||||
```yaml
|
||||
- uses: SonarSource/sonarqube-scan-action@<action version>
|
||||
with:
|
||||
skipSignatureVerification: true
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Signature verification requires `gpg` and `dirmngr` to be installed on the runner. GitHub-hosted runners include both, but some self-hosted runners or containers may not.
|
||||
>
|
||||
> **Version history:**
|
||||
> - Introduced in **v7.2** with a default value of `true` to avoid breaking existing workflows on runners without `dirmngr`.
|
||||
> - Changed to `false` by default in **v8** (breaking change). If your runner does not have `gpg` or `dirmngr` installed, set this option to `true` explicitly.
|
||||
|
||||
More information about possible analysis parameters can be found:
|
||||
* in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/analysis-parameters/) of the SonarQube Server documentation
|
||||
* in the [Analysis parameters page](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/analysis-parameters/) of the SonarQube Cloud documentation
|
||||
|
||||
|
||||
### Environment variables
|
||||
|
||||
- `SONAR_TOKEN` – **Required** this is the token used to authenticate access to SonarQube. You can read more about security tokens in the documentation of SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/user-guide/managing-tokens/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/managing-your-account/managing-tokens/). You can set the `SONAR_TOKEN` environment variable in the "Secrets" settings page of your repository, or you can add them at the level of your GitHub organization (recommended).
|
||||
@@ -298,33 +248,236 @@ If your source code file names contain special characters that are not covered b
|
||||
LC_ALL: "ru_RU.UTF-8"
|
||||
```
|
||||
|
||||
## Alternatives for Java and .NET
|
||||
## Workflow Examples
|
||||
|
||||
This GitHub Action will not work for all technologies. If you are in one of the following situations, you should use the following alternatives:
|
||||
|
||||
* Your code is built with Maven. Read the documentation about our SonarScanner for Maven in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner-for-maven/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-maven/).
|
||||
* Your code is built with Gradle. Read the documentation about our SonarScanner for Gradle in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-gradle/).
|
||||
* You want to analyze a .NET solution. Read the documentation about our SonarScanner for .NET in SonarQube [Server](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/dotnet/introduction/) and [Cloud](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/ci-based-analysis/sonarscanner-for-dotnet/introduction/).
|
||||
### For SonarQube Cloud
|
||||
|
||||
## Do not use this GitHub action if you are in the following situations
|
||||
Project metadata, including the location of the sources to be analyzed, must be declared in the file sonar-project.properties in the base directory:
|
||||
|
||||
* You want to run the action on C, C++, or Objective-C projects on a 32-bits system - build wrappers support only 64-bits OS.
|
||||
```properties
|
||||
sonar.organization=<replace with your SonarQube Cloud organization key>
|
||||
sonar.projectKey=<replace with the key generated when setting up the project on SonarQube Cloud>
|
||||
|
||||
## Self-hosted runner or container
|
||||
# relative paths to source directories. More details and properties are described
|
||||
# at https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/analysis-scope/
|
||||
sonar.sources=src
|
||||
```
|
||||
|
||||
|
||||
#### Standard Projects
|
||||
|
||||
For projects that:
|
||||
- do not contain C, C++, or Objective-C, and
|
||||
- for C, C++, Objective-C projects that don't use [Build Wrapper](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper)
|
||||
|
||||
the workflow, usually declared under `.github/workflows/build.yml`, looks like the following:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Disabling shallow clones is recommended for improving the relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version or sha1> # Ex: v4.1.0 or sha1, See the latest version at https://github.com/marketplace/actions/official-sonarqube-scan
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
```
|
||||
|
||||
#### C/C++/Objective-C with Build Wrapper
|
||||
|
||||
For C, C++, and Objective-C projects relying on [Build Wrapper](https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper) to generate the compilation database, the workflow requires additional steps to download the Build Wrapper and invoke it:
|
||||
|
||||
```yaml
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Disabling shallow clone is recommended for improving relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: Install Build Wrapper
|
||||
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@<action version>
|
||||
- name: Run Build Wrapper
|
||||
run: |
|
||||
# Here goes your compilation wrapped with Build Wrapper
|
||||
# For more information, see https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/languages/c-family/prerequisites/#using-build-wrapper
|
||||
# build-preparation steps
|
||||
# build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} build-command
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version or sha1>
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_ROOT_CERT: ${{ secrets.SONAR_ROOT_CERT }}
|
||||
with:
|
||||
# Consult https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner/ for more information and options
|
||||
args: >
|
||||
--define "sonar.cfamily.compile-commands=${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||
```
|
||||
|
||||
See also [example configurations of C++ projects for SonarQube Cloud](https://github.com/search?q=org%3Asonarsource-cfamily-examples+gh-actions-sc&type=repositories).
|
||||
|
||||
### For SonarQube Server
|
||||
|
||||
Project metadata, including the location of the sources to be analyzed, can be declared in the file `sonar-project.properties` in the base directory:
|
||||
|
||||
```properties
|
||||
sonar.projectKey=<replace with the key generated when setting up the project on SonarQube Server>
|
||||
|
||||
# relative paths to source directories. More details and properties are described
|
||||
# at https://docs.sonarsource.com/sonarqube-server/latest/project-administration/analysis-scope/
|
||||
sonar.sources=src
|
||||
```
|
||||
|
||||
#### Standard Projects
|
||||
|
||||
For projects that:
|
||||
- do not contain C, C++, or Objective-C, and
|
||||
- for C, C++, Objective-C projects that don't use [Build Wrapper](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper)
|
||||
the workflow, usually declared under `.github/workflows/build.yml`, looks like the following:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Disabling shallow clones is recommended for improving the relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version or sha1> # Ex: v4.1.0, or sha1, See the latest version at https://github.com/marketplace/actions/official-sonarqube-scan
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
```
|
||||
|
||||
#### C/C++/Objective-C with Build Wrapper
|
||||
|
||||
|
||||
|
||||
This subsection would contain the more complex YAML configuration for projects that require the
|
||||
build wrapper to generate a compilation database. The example would detail the three-step
|
||||
process: checking out the code, installing the build wrapper, and then running the SonarQube
|
||||
scan with the appropriate parameters.
|
||||
|
||||
For C, C++, and Objective-C projects relying on [Build Wrapper](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper) to generate the compilation database, the workflow requires additional steps to download the Build Wrapper and invoke it:
|
||||
|
||||
```yaml
|
||||
# Trigger analysis when pushing to your main branches, and when creating a pull request.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- 'releases/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Main Workflow
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Disabling shallow clone is recommended for improving relevancy of reporting
|
||||
fetch-depth: 0
|
||||
- name: Install Build Wrapper
|
||||
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@<action version>
|
||||
env:
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
- name: Run Build Wrapper
|
||||
run: |
|
||||
# Here goes your compilation wrapped with Build Wrapper
|
||||
# For more information, see https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/languages/c-family/prerequisites/#using-buildwrapper
|
||||
# build-preparation steps
|
||||
# build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} build-command
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@<action version or sha1>
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||
SONAR_ROOT_CERT: ${{ secrets.SONAR_ROOT_CERT }}
|
||||
with:
|
||||
# Consult https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner/ for more information and options
|
||||
args: >
|
||||
--define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||
```
|
||||
|
||||
If you are using SonarQube Server 10.5 or earlier, use `sonar.cfamily.build-wrapper-output` instead of `sonar.cfamily.compile-commands` in the `args` property of the last step, as Build Wrapper does not generate a `compile_commands.json` file before SonarQube Server 10.6.
|
||||
|
||||
It should look like this:
|
||||
|
||||
```yaml
|
||||
with:
|
||||
args: >
|
||||
--define "sonar.cfamily.build-wrapper-output=${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
||||
```
|
||||
|
||||
See also [example configurations of C++ projects for SonarQube Server](https://github.com/search?q=org%3Asonarsource-cfamily-examples+gh-actions-sq&type=repositories).
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Self-hosted runner or container
|
||||
|
||||
When running the action in a self-hosted runner or container, please ensure that the following programs are installed:
|
||||
|
||||
* **curl** or **wget**
|
||||
* **unzip**
|
||||
|
||||
## Additional information
|
||||
### Additional information
|
||||
|
||||
The `sonarqube-scan-action/install-build-wrapper` action installs `coreutils` if run on macOS.
|
||||
|
||||
## Have questions or feedback?
|
||||
## Support & Community
|
||||
|
||||
To provide feedback (requesting a feature or reporting a bug) please post on the SonarSource Community Forum page for SonarQube [Server](https://community.sonarsource.com/tags/c/help/sq/github-actions) or [Cloud](https://community.sonarsource.com/tags/c/help/sc/9/github-actions).
|
||||
To provide feedback (requesting a feature or reporting a bug) please post on the SonarSource Community Forum page for [SonarQube Server](https://community.sonarsource.com/tags/c/help/sq/github-actions) or [SonarQube Cloud](https://community.sonarsource.com/tags/c/help/sc/9/github-actions).
|
||||
|
||||
## License
|
||||
### License
|
||||
|
||||
Container images built with this project include third-party materials.
|
||||
|
||||
+9
-35
@@ -10,50 +10,24 @@ inputs:
|
||||
args:
|
||||
description: Additional arguments to the Sonar Scanner CLI
|
||||
required: false
|
||||
default: ""
|
||||
projectBaseDir:
|
||||
description: Set the sonar.projectBaseDir analysis property
|
||||
required: false
|
||||
default: "."
|
||||
scannerVersion:
|
||||
description: Version of the Sonar Scanner CLI to use
|
||||
required: false
|
||||
# to be kept in sync with sonar-scanner-version
|
||||
default: 7.2.0.5079
|
||||
default: 8.0.1.6346
|
||||
scannerBinariesUrl:
|
||||
description: URL to download the Sonar Scanner CLI binaries from
|
||||
required: false
|
||||
default: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
|
||||
skipSignatureVerification:
|
||||
description: Skip GPG signature verification (not recommended for security)
|
||||
required: false
|
||||
default: "false"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Sanity checks
|
||||
run: ${GITHUB_ACTION_PATH}/scripts/sanity-checks.sh
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_PROJECTBASEDIR: ${{ inputs.projectBaseDir }}
|
||||
INPUT_SCANNERVERSION: ${{ inputs.scannerVersion }}
|
||||
- name: Load Sonar Scanner CLI from cache
|
||||
id: sonar-scanner-cli
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
env:
|
||||
# The default value is 60mins. Reaching timeout is treated the same as a cache miss.
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
with:
|
||||
path: ${{ runner.temp }}/sonar-scanner-cli-${{ inputs.scannerVersion }}-${{ runner.os }}-${{ runner.arch }}
|
||||
key: sonar-scanner-cli-${{ inputs.scannerVersion }}-${{ runner.os }}-${{ runner.arch }}
|
||||
- name: Install Sonar Scanner CLI
|
||||
if: ${{ env.NO_CACHE == 'true' || steps.sonar-scanner-cli.outputs.cache-hit != 'true' }}
|
||||
run: ${GITHUB_ACTION_PATH}/scripts/install-sonar-scanner-cli.sh
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_SCANNERVERSION: ${{ inputs.scannerVersion }}
|
||||
INPUT_SCANNERBINARIESURL: ${{ inputs.scannerBinariesUrl }}
|
||||
- name: Add SonarScanner CLI to the PATH
|
||||
run: echo "${RUNNER_TEMP}/sonar-scanner-cli-${{ inputs.scannerVersion }}-${{ runner.os }}-${{ runner.arch }}/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- name: Run SonarScanner
|
||||
run: ${GITHUB_ACTION_PATH}/scripts/run-sonar-scanner.sh
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_ARGS: ${{ inputs.args }}
|
||||
INPUT_PROJECTBASEDIR: ${{ inputs.projectBaseDir }}
|
||||
SONAR_SCANNER_JRE: ${{ runner.temp }}/sonar-scanner-cli-${{ inputs.scannerVersion }}-${{ runner.os }}-${{ runner.arch }}/jre
|
||||
using: node24
|
||||
main: dist/index.js
|
||||
|
||||
+36
-4
@@ -1,17 +1,18 @@
|
||||
Contributing
|
||||
============
|
||||
# SonarQube Scan Action
|
||||
|
||||
## Contributing
|
||||
|
||||
If you would like to see a new feature, please create a new thread in the forum ["Suggest new features"](https://community.sonarsource.com/c/suggestions/features).
|
||||
|
||||
Please be aware that we are not actively looking for feature contributions. The truth is that it's extremely difficult for someone outside SonarSource to comply with our roadmap and expectations. Therefore, we typically only accept minor cosmetic changes and typo fixes.
|
||||
|
||||
## Submitting a pull request
|
||||
### Submitting a pull request
|
||||
|
||||
With that in mind, if you would like to submit a code contribution, please create a pull request for this repository. Please explain your motives to contribute this change: what problem you are trying to fix, what improvement you are trying to make.
|
||||
|
||||
Make sure that you follow our [code style](https://github.com/SonarSource/sonar-developer-toolset#code-style) and all tests are passing (Travis build is executed for each pull request).
|
||||
|
||||
## Next steps
|
||||
### Next steps
|
||||
|
||||
One of the members of our team will carefully review your pull request. You might be asked at this point for clarifications or your pull request might be rejected if we decide that it doesn't fit our roadmap and vision for the product.
|
||||
If your contribution looks promising then either we will decide:
|
||||
@@ -24,3 +25,34 @@ or
|
||||
|
||||
Thank You!
|
||||
The SonarSource Team
|
||||
|
||||
## Development
|
||||
|
||||
Both the main action and the secondary _install-build-wrapper_ action are [Javascript actions](https://docs.github.com/en/actions/tutorials/create-actions/create-a-javascript-action). They need to be packaged to work properly. We follow the official guidelines and rely on rollup for that.
|
||||
|
||||
### Requirements
|
||||
|
||||
Make sure you have node 24 & npm installed. We recommend using [nvm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm#using-a-node-version-manager-to-install-nodejs-and-npm) for that.
|
||||
|
||||
### Building & testing
|
||||
|
||||
You'll first need to install dependencies:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
To use rollup to bundle the scripts, run the `build` command:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
> ⚠️ Since the action uses the code in the repository, it is necessary to commit the bundled code! ⚠️
|
||||
|
||||
|
||||
To run the js unit tests, run the `test` command:
|
||||
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
|
||||
@@ -71,7 +71,7 @@ runs:
|
||||
- name: Cache sonar-scanner installation
|
||||
id: cache-sonar-tools
|
||||
if: inputs.cache-binaries == 'true'
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
env:
|
||||
# The default value is 60mins. Reaching timeout is treated the same as a cache miss.
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
|
||||
Vendored
+30514
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+27277
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+31833
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+4530
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+248
@@ -0,0 +1,248 @@
|
||||
import { f as execExports, h as addPath, a as info, n as setOutput, s as setFailed, o as startGroup, p as endGroup } from './exec-zlpfwmpH.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import 'os';
|
||||
import 'crypto';
|
||||
import 'http';
|
||||
import 'https';
|
||||
import 'net';
|
||||
import 'tls';
|
||||
import 'events';
|
||||
import 'assert';
|
||||
import 'util';
|
||||
import 'node:assert';
|
||||
import 'node:net';
|
||||
import 'node:http';
|
||||
import 'node:stream';
|
||||
import 'node:buffer';
|
||||
import 'node:util';
|
||||
import 'node:querystring';
|
||||
import 'node:events';
|
||||
import 'node:diagnostics_channel';
|
||||
import 'node:tls';
|
||||
import 'node:zlib';
|
||||
import 'node:perf_hooks';
|
||||
import 'node:util/types';
|
||||
import 'node:worker_threads';
|
||||
import 'node:url';
|
||||
import 'node:async_hooks';
|
||||
import 'node:console';
|
||||
import 'node:dns';
|
||||
import 'string_decoder';
|
||||
import 'child_process';
|
||||
import 'timers';
|
||||
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
/**
|
||||
* Compute all names and paths related to the build wrapper
|
||||
* based on the runner environment
|
||||
*/
|
||||
function getBuildWrapperInfo({
|
||||
runnerOS,
|
||||
runnerArch,
|
||||
runnerTemp,
|
||||
sonarHostUrl,
|
||||
}) {
|
||||
const { buildWrapperSuffix, buildWrapperName } = getSuffixAndName(
|
||||
runnerOS,
|
||||
runnerArch
|
||||
);
|
||||
|
||||
const buildWrapperDir = `${runnerTemp}/build-wrapper-${buildWrapperSuffix}`;
|
||||
const buildWrapperUrl = `${sonarHostUrl}/static/cpp/build-wrapper-${buildWrapperSuffix}.zip`;
|
||||
const buildWrapperBin = `${buildWrapperDir}/${buildWrapperName}`;
|
||||
|
||||
return {
|
||||
buildWrapperUrl,
|
||||
buildWrapperDir,
|
||||
buildWrapperBin,
|
||||
};
|
||||
}
|
||||
|
||||
function getSuffixAndName(runnerOS, runnerArch) {
|
||||
if (
|
||||
runnerArch !== "X64" &&
|
||||
!(runnerArch === "ARM64" && (runnerOS === "macOS" || runnerOS === "Linux"))
|
||||
) {
|
||||
throw new Error(
|
||||
`Architecture '${runnerArch}' is unsupported by build-wrapper`
|
||||
);
|
||||
}
|
||||
|
||||
switch (runnerOS) {
|
||||
case "Windows":
|
||||
return {
|
||||
buildWrapperSuffix: "win-x86",
|
||||
buildWrapperName: "build-wrapper-win-x86-64.exe",
|
||||
};
|
||||
|
||||
case "Linux":
|
||||
switch (runnerArch) {
|
||||
case "X64":
|
||||
return {
|
||||
buildWrapperSuffix: "linux-x86",
|
||||
buildWrapperName: "build-wrapper-linux-x86-64",
|
||||
};
|
||||
|
||||
case "ARM64":
|
||||
return {
|
||||
buildWrapperSuffix: "linux-aarch64",
|
||||
buildWrapperName: "build-wrapper-linux-aarch64",
|
||||
};
|
||||
}
|
||||
break; // handled before the switch
|
||||
|
||||
case "macOS":
|
||||
return {
|
||||
buildWrapperSuffix: "macosx-x86",
|
||||
buildWrapperName: "build-wrapper-macosx-x86",
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported runner OS '${runnerOS}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRealPath(filePath, runnerOS) {
|
||||
switch (runnerOS) {
|
||||
case "Windows": {
|
||||
const windowsResult = await execExports.getExecOutput("cygpath", [
|
||||
"--absolute",
|
||||
"--windows",
|
||||
filePath,
|
||||
]);
|
||||
return windowsResult.stdout.trim();
|
||||
}
|
||||
case "Linux": {
|
||||
const linuxResult = await execExports.getExecOutput("readlink", [
|
||||
"-f",
|
||||
filePath,
|
||||
]);
|
||||
return linuxResult.stdout.trim();
|
||||
}
|
||||
case "macOS": {
|
||||
const macResult = await execExports.getExecOutput("greadlink", ["-f", filePath]);
|
||||
return macResult.stdout.trim();
|
||||
}
|
||||
default:
|
||||
return path.resolve(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
async function installMacOSPackages() {
|
||||
if (process.platform === "darwin") {
|
||||
info("Installing required packages for macOS");
|
||||
await execExports.exec("brew", ["install", "coreutils"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These RUNNER_XX env variables come from GitHub by default.
|
||||
* See https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
|
||||
*
|
||||
* If SONAR_HOST_URL is omitted, we assume sonarcloud.io
|
||||
*/
|
||||
function getEnvVariables() {
|
||||
const sonarHostUrl = process.env.SONAR_HOST_URL
|
||||
? process.env.SONAR_HOST_URL.replace(/\/$/, "")
|
||||
: "https://sonarcloud.io";
|
||||
|
||||
return {
|
||||
runnerOS: process.env.RUNNER_OS,
|
||||
runnerArch: process.env.RUNNER_ARCH,
|
||||
runnerTemp: process.env.RUNNER_TEMP,
|
||||
sonarHostUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadAndInstallBuildWrapper(downloadUrl, runnerEnv) {
|
||||
const { runnerArch, runnerOS, runnerTemp } = runnerEnv;
|
||||
const tmpZipPath = path.join(
|
||||
runnerTemp,
|
||||
`build-wrapper-${runnerOS}-${runnerArch}.zip`
|
||||
);
|
||||
|
||||
startGroup(`Download ${downloadUrl}`);
|
||||
|
||||
info(`Downloading '${downloadUrl}'`);
|
||||
|
||||
if (!fs.existsSync(runnerTemp)) {
|
||||
fs.mkdirSync(runnerTemp, { recursive: true });
|
||||
}
|
||||
|
||||
await execExports.exec("curl", ["-sSLo", tmpZipPath, downloadUrl]);
|
||||
|
||||
info("Decompressing");
|
||||
await execExports.exec("unzip", ["-o", "-d", runnerTemp, tmpZipPath]);
|
||||
|
||||
endGroup();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await installMacOSPackages();
|
||||
|
||||
const envVariables = getEnvVariables();
|
||||
|
||||
const { buildWrapperBin, buildWrapperDir, buildWrapperUrl } =
|
||||
getBuildWrapperInfo(envVariables);
|
||||
|
||||
await downloadAndInstallBuildWrapper(buildWrapperUrl, envVariables);
|
||||
|
||||
const buildWrapperBinDir = await getRealPath(
|
||||
buildWrapperDir,
|
||||
envVariables.runnerOS
|
||||
);
|
||||
addPath(buildWrapperBinDir);
|
||||
info(`'${buildWrapperBinDir}' added to the path`);
|
||||
|
||||
const buildWrapperBinPath = await getRealPath(
|
||||
buildWrapperBin,
|
||||
envVariables.runnerOS
|
||||
);
|
||||
setOutput("build-wrapper-binary", buildWrapperBinPath);
|
||||
info(`'build-wrapper-binary' output set to '${buildWrapperBinPath}'`);
|
||||
} catch (error) {
|
||||
setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
//# sourceMappingURL=install-build-wrapper.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
name: 'Install Build Wrapper for C and C++'
|
||||
name: "Install Build Wrapper for C and C++"
|
||||
description: >
|
||||
Download and install the Build Wrapper for C, C++, and Objective-C
|
||||
projects analyzed with manual config.
|
||||
@@ -8,55 +8,6 @@ branding:
|
||||
outputs:
|
||||
build-wrapper-binary:
|
||||
description: "Absolute path to Build Wrapper binary."
|
||||
value: ${{ steps.setup-outputs.outputs.build-wrapper-binary }}
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
# install packaged required for greadlink and sha256sum command on macOS
|
||||
- name: Install required packages for macOS
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: brew install coreutils
|
||||
|
||||
- name: Set SONAR_HOST_URL to 'https://sonarcloud.io'
|
||||
if: env.SONAR_HOST_URL == ''
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Setting SONAR_HOST_URL to 'https://sonarcloud.io'"
|
||||
echo "SONAR_HOST_URL=https://sonarcloud.io" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure paths
|
||||
id: configure_paths
|
||||
shell: bash
|
||||
env:
|
||||
OS: ${{ runner.os }}
|
||||
ARCH: ${{ runner.arch }}
|
||||
INSTALL_PATH: ${{ runner.temp }} # TODO: or .sonar, if RUNNER_TEMP creates problem with caching and self-hosted runners
|
||||
run: ${GITHUB_ACTION_PATH}/../scripts/configure_paths.sh >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download and install Build Wrapper
|
||||
shell: bash
|
||||
env:
|
||||
DOWNLOAD_URL: ${{ steps.configure_paths.outputs.build-wrapper-url }}
|
||||
TMP_ZIP_PATH: ${{ runner.temp }}/build-wrapper-${{ inputs.configure_paths.sonar-scanner-version }}-${{ runner.os }}-${{ runner.arch }}.zip
|
||||
INSTALL_PATH: ${{ runner.temp }} # TODO: or .sonar, if RUNNER_TEMP creates problem with caching and self-hosted runners
|
||||
run: ${GITHUB_ACTION_PATH}/../scripts/download.sh
|
||||
|
||||
- name: Setup action outputs
|
||||
id: setup-outputs
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_WRAPPER_DIR: ${{ steps.configure_paths.outputs.build-wrapper-dir }}
|
||||
BUILD_WRAPPER_BIN: ${{ steps.configure_paths.outputs.build-wrapper-bin }}
|
||||
run: |
|
||||
source ${GITHUB_ACTION_PATH}/../scripts/utils.sh
|
||||
|
||||
BUILD_WRAPPER_BIN_DIR=$(realpath "${BUILD_WRAPPER_DIR}")
|
||||
echo "${BUILD_WRAPPER_BIN_DIR}" >> $GITHUB_PATH
|
||||
echo "'${BUILD_WRAPPER_BIN_DIR}' added to the path"
|
||||
|
||||
BUILD_WRAPPER_BIN=$(realpath "${BUILD_WRAPPER_BIN}")
|
||||
echo "build-wrapper-binary=${BUILD_WRAPPER_BIN}" >> $GITHUB_OUTPUT
|
||||
echo "'build-wrapper-binary' output set to '${BUILD_WRAPPER_BIN}'"
|
||||
echo "::endgroup::"
|
||||
using: node24
|
||||
main: ../dist/install-build-wrapper.js
|
||||
|
||||
Generated
+1041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "sonarqube-scan-action",
|
||||
"version": "6.0.0",
|
||||
"description": "This SonarSource project, available as a GitHub Action, scans your projects with SonarQube [Server](https://www.sonarsource.com/products/sonarqube/) or [Cloud](https://www.sonarsource.com/products/sonarcloud/).",
|
||||
"type": "module",
|
||||
"main": "src/main/index.js",
|
||||
"scripts": {
|
||||
"build": "rollup --config rollup.config.js",
|
||||
"test": "node --experimental-test-module-mocks --test"
|
||||
},
|
||||
"license": "LGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@actions/core": "3.0.0",
|
||||
"@actions/exec": "2.0.0",
|
||||
"@actions/github": "9.0.0",
|
||||
"@actions/tool-cache": "4.0.0",
|
||||
"string-argv": "0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "29.0.2",
|
||||
"@rollup/plugin-node-resolve": "16.0.3",
|
||||
"mock-fs": "5.5.0",
|
||||
"rollup": "4.60.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
|
||||
const config = {
|
||||
input: [
|
||||
"src/main/index.js",
|
||||
"src/install-build-wrapper/install-build-wrapper.js",
|
||||
],
|
||||
output: {
|
||||
esModule: true,
|
||||
dir: "dist",
|
||||
format: "es",
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [commonjs(), nodeResolve({ preferBuiltins: true })],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
if [[ -n "${SONAR_ROOT_CERT}" ]]; then
|
||||
echo "Adding custom root certificate to java certificate store"
|
||||
rm -f /tmp/tmpcert.pem
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
if [[ ${ARCH} != "X64" && ! (${ARCH} == "ARM64" && (${OS} == "macOS" || ${OS} == "Linux")) ]]; then
|
||||
echo "::error::Architecture '${ARCH}' is unsupported by build-wrapper"
|
||||
exit 1
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
source "$(dirname -- "$0")/utils.sh"
|
||||
|
||||
echo "Installation path is '${INSTALL_PATH}'"
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
source "$(dirname -- "$0")/utils.sh"
|
||||
|
||||
VERIFY_CORRECTNESS=false
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
source "$(dirname -- "$0")/utils.sh"
|
||||
|
||||
SONAR_SCANNER_VERSION=$(curl -sSL -H "Accept: application/vnd.github+json" \
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eou pipefail
|
||||
|
||||
# See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
|
||||
#
|
||||
# Script-specific variables required:
|
||||
# - INPUT_SCANNERVERSION: e.g. 6.2.1.4610
|
||||
# - INPUT_SCANNERBINARIESURL: e.g. https://github.com/me/my-repo/raw/refs/heads/main/binaries
|
||||
|
||||
if [[ "$RUNNER_OS" == "Linux" && "$RUNNER_ARCH" == "X64" ]]; then
|
||||
FLAVOR="linux-x64"
|
||||
elif [[ "$RUNNER_OS" == "Linux" && "$RUNNER_ARCH" == "ARM64" ]]; then
|
||||
FLAVOR="linux-aarch64"
|
||||
elif [[ "$RUNNER_OS" == "Windows" && "$RUNNER_ARCH" == "X64" ]]; then
|
||||
FLAVOR="windows-x64"
|
||||
elif [[ "$RUNNER_OS" == "macOS" && "$RUNNER_ARCH" == "X64" ]]; then
|
||||
FLAVOR="macosx-x64"
|
||||
elif [[ "$RUNNER_OS" == "macOS" && "$RUNNER_ARCH" == "ARM64" ]]; then
|
||||
FLAVOR="macosx-aarch64"
|
||||
else
|
||||
echo "::error title=SonarScanner::$RUNNER_OS $RUNNER_ARCH not supported"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
mkdir -p $RUNNER_TEMP/sonarscanner
|
||||
cd $RUNNER_TEMP/sonarscanner
|
||||
|
||||
SCANNER_FILE_NAME="sonar-scanner-cli-$INPUT_SCANNERVERSION-$FLAVOR.zip"
|
||||
SCANNER_URI="${INPUT_SCANNERBINARIESURL%/}/$SCANNER_FILE_NAME"
|
||||
|
||||
if command -v wget &> /dev/null; then
|
||||
wget --no-verbose --user-agent=sonarqube-scan-action "$SCANNER_URI"
|
||||
elif command -v curl &> /dev/null; then
|
||||
curl --fail --silent --show-error --user-agent sonarqube-scan-action \
|
||||
--location --output "$SCANNER_FILE_NAME" "$SCANNER_URI"
|
||||
elif [ "$RUNNER_OS" == "Windows" ] && [ -t "C:\\msys64\\usr\\bin\\wget.exe" ]; then
|
||||
"C:\\msys64\\usr\\bin\\wget.exe" --no-verbose --user-agent=sonarqube-scan-action "$SCANNER_URI"
|
||||
elif [ "$RUNNER_OS" == "Windows" ] && [ -t "C:\\msys64\\usr\\bin\\curl.exe" ]; then
|
||||
"C:\\msys64\\usr\\bin\\curl.exe" --fail --silent --show-error --user-agent sonarqube-scan-action \
|
||||
--location --output "$SCANNER_FILE_NAME" "$SCANNER_URI"
|
||||
else
|
||||
echo "::error title=SonarScanner::Neither wget nor curl found on the machine"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unzip -q -o $SCANNER_FILE_NAME
|
||||
|
||||
SCANNER_UNZIP_FOLDER="sonar-scanner-$INPUT_SCANNERVERSION-$FLAVOR"
|
||||
# Folder name should correspond to the directory cached by the actions/cache
|
||||
SCANNER_LOCAL_FOLDER="$RUNNER_TEMP/sonar-scanner-cli-$INPUT_SCANNERVERSION-$RUNNER_OS-$RUNNER_ARCH"
|
||||
|
||||
if [ -d "$SCANNER_LOCAL_FOLDER" ]; then
|
||||
echo "::warning title=SonarScanner::Cleaning existing scanner folder: $SCANNER_LOCAL_FOLDER"
|
||||
rm -rf "$SCANNER_LOCAL_FOLDER"
|
||||
fi
|
||||
|
||||
mv -f "$SCANNER_UNZIP_FOLDER" "$SCANNER_LOCAL_FOLDER"
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
SCANNER_BIN="sonar-scanner.bat"
|
||||
else
|
||||
SCANNER_BIN="sonar-scanner"
|
||||
fi
|
||||
|
||||
scanner_args=()
|
||||
if [[ ${SONARCLOUD_URL} ]]; then
|
||||
scanner_args+=("-Dsonar.scanner.sonarcloudUrl=${SONARCLOUD_URL}")
|
||||
fi
|
||||
|
||||
if [[ "$RUNNER_DEBUG" == '1' ]]; then
|
||||
scanner_args+=('--debug')
|
||||
fi
|
||||
|
||||
if [[ -n "${INPUT_PROJECTBASEDIR}" ]]; then
|
||||
scanner_args+=("-Dsonar.projectBaseDir=${INPUT_PROJECTBASEDIR}")
|
||||
fi
|
||||
|
||||
# The SSL folder may exist on an uncleaned self-hosted runner
|
||||
SONAR_SSL_FOLDER=~/.sonar/ssl
|
||||
# Use keytool for now, as SonarQube 10.6 and below doesn't support openssl generated keystores
|
||||
# keytool requires a password > 6 characters, so we won't use the default password 'sonar'
|
||||
KEYTOOL_MAIN_CLASS=sun.security.tools.keytool.Main
|
||||
SONAR_SSL_TRUSTSTORE_FILE="$SONAR_SSL_FOLDER/truststore.p12"
|
||||
SONAR_SSL_TRUSTSTORE_PASSWORD=changeit
|
||||
|
||||
if [ -f "$SONAR_SSL_TRUSTSTORE_FILE" ]; then
|
||||
ALIAS_SONAR_IS_PRESENT=true
|
||||
|
||||
"$SONAR_SCANNER_JRE/bin/java" "$KEYTOOL_MAIN_CLASS" \
|
||||
-storetype PKCS12 \
|
||||
-keystore "$SONAR_SSL_TRUSTSTORE_FILE" \
|
||||
-storepass "$SONAR_SSL_TRUSTSTORE_PASSWORD" \
|
||||
-noprompt \
|
||||
-trustcacerts \
|
||||
-list -v -alias sonar > /dev/null 2>&1 || {
|
||||
ALIAS_SONAR_IS_PRESENT=false
|
||||
echo "Existing Scanner truststore $SONAR_SSL_TRUSTSTORE_FILE does not contain 'sonar' alias"
|
||||
}
|
||||
|
||||
if [[ $ALIAS_SONAR_IS_PRESENT == "true" ]]; then
|
||||
echo "Removing 'sonar' alias from already existing Scanner truststore: $SONAR_SSL_TRUSTSTORE_FILE"
|
||||
"$SONAR_SCANNER_JRE/bin/java" "$KEYTOOL_MAIN_CLASS" \
|
||||
-storetype PKCS12 \
|
||||
-keystore "$SONAR_SSL_TRUSTSTORE_FILE" \
|
||||
-storepass "$SONAR_SSL_TRUSTSTORE_PASSWORD" \
|
||||
-noprompt \
|
||||
-trustcacerts \
|
||||
-delete \
|
||||
-alias sonar
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${SONAR_ROOT_CERT}" ]]; then
|
||||
echo "Adding SSL certificate to the Scanner truststore"
|
||||
rm -f $RUNNER_TEMP/tmpcert.pem
|
||||
echo "${SONAR_ROOT_CERT}" > $RUNNER_TEMP/tmpcert.pem
|
||||
mkdir -p "$SONAR_SSL_FOLDER"
|
||||
"$SONAR_SCANNER_JRE/bin/java" "$KEYTOOL_MAIN_CLASS" \
|
||||
-storetype PKCS12 \
|
||||
-keystore "$SONAR_SSL_TRUSTSTORE_FILE" \
|
||||
-storepass "$SONAR_SSL_TRUSTSTORE_PASSWORD" \
|
||||
-noprompt \
|
||||
-trustcacerts \
|
||||
-importcert \
|
||||
-alias sonar \
|
||||
-file "$RUNNER_TEMP/tmpcert.pem"
|
||||
scanner_args+=("-Dsonar.scanner.truststorePassword=$SONAR_SSL_TRUSTSTORE_PASSWORD")
|
||||
fi
|
||||
|
||||
# split input args correctly (passed through INPUT_ARGS env var to avoid execution of injected command)
|
||||
args=()
|
||||
if [[ -n "${INPUT_ARGS}" ]]; then
|
||||
# the regex recognizes args with values in single or double quotes (without character escaping), and args without quotes as well
|
||||
# more specifically, the following patterns: -Darg="value", -Darg='value', -Darg=value, "-Darg=value" and '-Darg=value'
|
||||
IFS=$'\n'; args=($(echo ${INPUT_ARGS} | egrep -o '[^" '\'']+="[^"]*"|[^" '\'']+='\''[^'\'']*'\''|[^" '\'']+|"[^"]+"|'\''[^'\'']+'\'''))
|
||||
fi
|
||||
|
||||
for arg in "${args[@]}"; do
|
||||
scanner_args+=("$arg")
|
||||
done
|
||||
|
||||
set -ux
|
||||
|
||||
$SCANNER_BIN ${scanner_args[@]+"${scanner_args[@]}"}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# run the sonar scanner cli
|
||||
cmd=(${GITHUB_ACTION_PATH}/scripts/run-sonar-scanner-cli.sh "${INPUT_ARGS}")
|
||||
"${cmd[@]}"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if [[ ! "${INPUT_SCANNERVERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error title=SonarScanner::Invalid scannerVersion format. Expected format: x.y.z.w (e.g., 7.1.0.4889)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${SONAR_TOKEN}" ]]; then
|
||||
echo "::warning title=SonarScanner::Running this GitHub Action without SONAR_TOKEN is not recommended"
|
||||
fi
|
||||
|
||||
if [[ -f "${INPUT_PROJECTBASEDIR%/}/pom.xml" ]]; then
|
||||
echo "::warning title=SonarScanner::Maven project detected. Sonar recommends running the 'org.sonarsource.scanner.maven:sonar-maven-plugin:sonar' goal during the build process instead of using this GitHub Action
|
||||
to get more accurate results."
|
||||
fi
|
||||
|
||||
if [[ -f "${INPUT_PROJECTBASEDIR%/}/build.gradle" || -f "${INPUT_PROJECTBASEDIR%/}/build.gradle.kts" ]]; then
|
||||
echo "::warning title=SonarScanner::Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action
|
||||
to get more accurate results."
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
check_status() {
|
||||
exit_status=$?
|
||||
if [ $exit_status -ne 0 ]; then
|
||||
|
||||
+11
-11
@@ -1,11 +1,11 @@
|
||||
sonar-scanner-version=7.2.0.5079
|
||||
sonar-scanner-url-windows-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-windows-x64.zip
|
||||
sonar-scanner-sha-windows-x64=71936f352206b63cb05ffbcd68e366e52d22916148cf4a2418789bc776f733ea
|
||||
sonar-scanner-url-linux-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-x64.zip
|
||||
sonar-scanner-sha-linux-x64=da9f4e64a3d555f08ce38b5469ebd91fe2b311af473f7001a5ee5c1fd58b004b
|
||||
sonar-scanner-url-linux-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-aarch64.zip
|
||||
sonar-scanner-sha-linux-aarch64=803ca725d463e95eeb7537515706367bb8e52bf05ac32174daf9773bdb36d1e2
|
||||
sonar-scanner-url-macosx-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-macosx-x64.zip
|
||||
sonar-scanner-sha-macosx-x64=7b9e92248ca740fff41503bfe5459c460bac43c501d80043cc4fbebb72dfc5fa
|
||||
sonar-scanner-url-macosx-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-macosx-aarch64.zip
|
||||
sonar-scanner-sha-macosx-aarch64=c8adb3fbfe5485c17de193a217be765b66cbc10d6540057655afa3c3b5be6f61
|
||||
sonar-scanner-version=8.0.1.6346
|
||||
sonar-scanner-url-windows-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-windows-x64.zip
|
||||
sonar-scanner-sha-windows-x64=52b35b24be4ce5ec2e2933b32683db45db139581c46945546d9739b0c8866231
|
||||
sonar-scanner-url-linux-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-linux-x64.zip
|
||||
sonar-scanner-sha-linux-x64=4bd40bf8411ed104853e94a3746ec92bc92845fde2b27dbf5c33fb5cfa8ecbe9
|
||||
sonar-scanner-url-linux-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-linux-aarch64.zip
|
||||
sonar-scanner-sha-linux-aarch64=ae2b062ed6d640ab9014ab576042385d54c910857de952f5cb2592d2a2d7c8d8
|
||||
sonar-scanner-url-macosx-x64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-macosx-x64.zip
|
||||
sonar-scanner-sha-macosx-x64=aa9065347ba834ff6f3d461183eb40a67a321e6996206875fd257e8e7d5745b2
|
||||
sonar-scanner-url-macosx-aarch64=https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.0.1.6346-macosx-aarch64.zip
|
||||
sonar-scanner-sha-macosx-aarch64=2d65d49c327ec8ca5ec7c6dc2af17749f5b43c596fd906501bba5a0b09edc5e2
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { getBuildWrapperInfo } from "../utils.js";
|
||||
|
||||
describe("getBuildWrapperInfo", () => {
|
||||
const supportedPlatforms = [
|
||||
{
|
||||
platform: "Linux",
|
||||
arch: "X64",
|
||||
expectedSuffix: "linux-x86",
|
||||
expectedName: "build-wrapper-linux-x86-64",
|
||||
},
|
||||
{
|
||||
platform: "Linux",
|
||||
arch: "ARM64",
|
||||
expectedSuffix: "linux-aarch64",
|
||||
expectedName: "build-wrapper-linux-aarch64",
|
||||
},
|
||||
{
|
||||
platform: "Windows",
|
||||
arch: "X64",
|
||||
expectedSuffix: "win-x86",
|
||||
expectedName: "build-wrapper-win-x86-64.exe",
|
||||
},
|
||||
{
|
||||
platform: "macOS",
|
||||
arch: "X64",
|
||||
expectedSuffix: "macosx-x86",
|
||||
expectedName: "build-wrapper-macosx-x86",
|
||||
},
|
||||
{
|
||||
platform: "macOS",
|
||||
arch: "ARM64",
|
||||
expectedSuffix: "macosx-x86",
|
||||
expectedName: "build-wrapper-macosx-x86",
|
||||
},
|
||||
];
|
||||
|
||||
const unsupportedPlatforms = [
|
||||
{ platform: "linux", arch: "arm" },
|
||||
{ platform: "openbsd", arch: "X64" },
|
||||
{ platform: undefined, arch: "X64" },
|
||||
{ platform: "Linux", arch: undefined },
|
||||
{ platform: null, arch: "X64" },
|
||||
{ platform: "Linux", arch: null },
|
||||
];
|
||||
|
||||
supportedPlatforms.forEach(
|
||||
({ platform, arch, expectedSuffix, expectedName }) => {
|
||||
it(`returns ${expectedName} for ${platform} ${arch}`, () => {
|
||||
const result = getBuildWrapperInfo({
|
||||
runnerOS: platform,
|
||||
runnerArch: arch,
|
||||
runnerTemp: "/tmp",
|
||||
sonarHostUrl: "https://sonarcloud.io"
|
||||
});
|
||||
assert.equal(result.buildWrapperUrl, `https://sonarcloud.io/static/cpp/build-wrapper-${expectedSuffix}.zip`);
|
||||
assert.equal(result.buildWrapperDir, `/tmp/build-wrapper-${expectedSuffix}`);
|
||||
assert.equal(result.buildWrapperBin, `/tmp/build-wrapper-${expectedSuffix}/${expectedName}`);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
unsupportedPlatforms.forEach(({ platform, arch }) => {
|
||||
it(`throws for unsupported platform ${platform} ${arch}`, () => {
|
||||
assert.throws(
|
||||
() => getBuildWrapperInfo({
|
||||
runnerOS: platform,
|
||||
runnerArch: arch,
|
||||
runnerTemp: "/tmp",
|
||||
sonarHostUrl: "https://sonarcloud.io"
|
||||
}),
|
||||
(error) => {
|
||||
return error.message.includes('unsupported') || error.message.includes('Unsupported');
|
||||
},
|
||||
`should have thrown for ${platform} ${arch}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { getBuildWrapperInfo, getRealPath } from "./utils";
|
||||
|
||||
async function installMacOSPackages() {
|
||||
if (process.platform === "darwin") {
|
||||
core.info("Installing required packages for macOS");
|
||||
await exec.exec("brew", ["install", "coreutils"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These RUNNER_XX env variables come from GitHub by default.
|
||||
* See https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
|
||||
*
|
||||
* If SONAR_HOST_URL is omitted, we assume sonarcloud.io
|
||||
*/
|
||||
function getEnvVariables() {
|
||||
const sonarHostUrl = process.env.SONAR_HOST_URL
|
||||
? process.env.SONAR_HOST_URL.replace(/\/$/, "")
|
||||
: "https://sonarcloud.io";
|
||||
|
||||
return {
|
||||
runnerOS: process.env.RUNNER_OS,
|
||||
runnerArch: process.env.RUNNER_ARCH,
|
||||
runnerTemp: process.env.RUNNER_TEMP,
|
||||
sonarHostUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadAndInstallBuildWrapper(downloadUrl, runnerEnv) {
|
||||
const { runnerArch, runnerOS, runnerTemp } = runnerEnv;
|
||||
const tmpZipPath = path.join(
|
||||
runnerTemp,
|
||||
`build-wrapper-${runnerOS}-${runnerArch}.zip`
|
||||
);
|
||||
|
||||
core.startGroup(`Download ${downloadUrl}`);
|
||||
|
||||
core.info(`Downloading '${downloadUrl}'`);
|
||||
|
||||
if (!fs.existsSync(runnerTemp)) {
|
||||
fs.mkdirSync(runnerTemp, { recursive: true });
|
||||
}
|
||||
|
||||
await exec.exec("curl", ["-sSLo", tmpZipPath, downloadUrl]);
|
||||
|
||||
core.info("Decompressing");
|
||||
await exec.exec("unzip", ["-o", "-d", runnerTemp, tmpZipPath]);
|
||||
|
||||
core.endGroup();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await installMacOSPackages();
|
||||
|
||||
const envVariables = getEnvVariables();
|
||||
|
||||
const { buildWrapperBin, buildWrapperDir, buildWrapperUrl } =
|
||||
getBuildWrapperInfo(envVariables);
|
||||
|
||||
await downloadAndInstallBuildWrapper(buildWrapperUrl, envVariables);
|
||||
|
||||
const buildWrapperBinDir = await getRealPath(
|
||||
buildWrapperDir,
|
||||
envVariables.runnerOS
|
||||
);
|
||||
core.addPath(buildWrapperBinDir);
|
||||
core.info(`'${buildWrapperBinDir}' added to the path`);
|
||||
|
||||
const buildWrapperBinPath = await getRealPath(
|
||||
buildWrapperBin,
|
||||
envVariables.runnerOS
|
||||
);
|
||||
core.setOutput("build-wrapper-binary", buildWrapperBinPath);
|
||||
core.info(`'build-wrapper-binary' output set to '${buildWrapperBinPath}'`);
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,116 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as exec from "@actions/exec";
|
||||
import * as path from "path";
|
||||
|
||||
/**
|
||||
* Compute all names and paths related to the build wrapper
|
||||
* based on the runner environment
|
||||
*/
|
||||
export function getBuildWrapperInfo({
|
||||
runnerOS,
|
||||
runnerArch,
|
||||
runnerTemp,
|
||||
sonarHostUrl,
|
||||
}) {
|
||||
const { buildWrapperSuffix, buildWrapperName } = getSuffixAndName(
|
||||
runnerOS,
|
||||
runnerArch
|
||||
);
|
||||
|
||||
const buildWrapperDir = `${runnerTemp}/build-wrapper-${buildWrapperSuffix}`;
|
||||
const buildWrapperUrl = `${sonarHostUrl}/static/cpp/build-wrapper-${buildWrapperSuffix}.zip`;
|
||||
const buildWrapperBin = `${buildWrapperDir}/${buildWrapperName}`;
|
||||
|
||||
return {
|
||||
buildWrapperUrl,
|
||||
buildWrapperDir,
|
||||
buildWrapperBin,
|
||||
};
|
||||
}
|
||||
|
||||
function getSuffixAndName(runnerOS, runnerArch) {
|
||||
if (
|
||||
runnerArch !== "X64" &&
|
||||
!(runnerArch === "ARM64" && (runnerOS === "macOS" || runnerOS === "Linux"))
|
||||
) {
|
||||
throw new Error(
|
||||
`Architecture '${runnerArch}' is unsupported by build-wrapper`
|
||||
);
|
||||
}
|
||||
|
||||
switch (runnerOS) {
|
||||
case "Windows":
|
||||
return {
|
||||
buildWrapperSuffix: "win-x86",
|
||||
buildWrapperName: "build-wrapper-win-x86-64.exe",
|
||||
};
|
||||
|
||||
case "Linux":
|
||||
switch (runnerArch) {
|
||||
case "X64":
|
||||
return {
|
||||
buildWrapperSuffix: "linux-x86",
|
||||
buildWrapperName: "build-wrapper-linux-x86-64",
|
||||
};
|
||||
|
||||
case "ARM64":
|
||||
return {
|
||||
buildWrapperSuffix: "linux-aarch64",
|
||||
buildWrapperName: "build-wrapper-linux-aarch64",
|
||||
};
|
||||
}
|
||||
break; // handled before the switch
|
||||
|
||||
case "macOS":
|
||||
return {
|
||||
buildWrapperSuffix: "macosx-x86",
|
||||
buildWrapperName: "build-wrapper-macosx-x86",
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported runner OS '${runnerOS}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRealPath(filePath, runnerOS) {
|
||||
switch (runnerOS) {
|
||||
case "Windows": {
|
||||
const windowsResult = await exec.getExecOutput("cygpath", [
|
||||
"--absolute",
|
||||
"--windows",
|
||||
filePath,
|
||||
]);
|
||||
return windowsResult.stdout.trim();
|
||||
}
|
||||
case "Linux": {
|
||||
const linuxResult = await exec.getExecOutput("readlink", [
|
||||
"-f",
|
||||
filePath,
|
||||
]);
|
||||
return linuxResult.stdout.trim();
|
||||
}
|
||||
case "macOS": {
|
||||
const macResult = await exec.getExecOutput("greadlink", ["-f", filePath]);
|
||||
return macResult.stdout.trim();
|
||||
}
|
||||
default:
|
||||
return path.resolve(filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
* sonarqube-scan-action
|
||||
* Copyright (C) 2025 SonarSource SA
|
||||
* mailto:info AT sonarsource DOT com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import {afterEach, describe, it, mock} from "node:test";
|
||||
import {setupGpgHome,} from "../gpg-verification.js";
|
||||
|
||||
/**
|
||||
* Helper function to create a temporary GPG home directory for testing.
|
||||
* @param {Array} tempDirs - Array to track temp directories for cleanup
|
||||
* @returns {string} Path to the created GPG home directory
|
||||
*/
|
||||
function createTrackedGpgHome(tempDirs) {
|
||||
const gpgHome = setupGpgHome();
|
||||
tempDirs.push(gpgHome);
|
||||
assert.ok(fs.existsSync(gpgHome));
|
||||
return gpgHome;
|
||||
}
|
||||
|
||||
describe("gpg-verification with mocked exec", () => {
|
||||
let tempDirs = [];
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directories
|
||||
tempDirs.forEach((dir) => {
|
||||
try {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
describe("isGpgAvailable", () => {
|
||||
it("should return true when GPG is available", async (t) => {
|
||||
const execFn = mock.fn(async () => 0);
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { isGpgAvailable } = await import("../gpg-verification.js?test=gpg-available");
|
||||
|
||||
const result = await isGpgAvailable();
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.equal(execFn.mock.calls.length, 1);
|
||||
assert.equal(execFn.mock.calls[0].arguments[0], "gpg");
|
||||
assert.deepEqual(execFn.mock.calls[0].arguments[1], ["--version"]);
|
||||
});
|
||||
|
||||
it("should return false when GPG is not available", async (t) => {
|
||||
const execFn = mock.fn(async () => {
|
||||
throw new Error("GPG not found");
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { isGpgAvailable } = await import("../gpg-verification.js?test=gpg-unavailable");
|
||||
|
||||
const result = await isGpgAvailable();
|
||||
|
||||
assert.equal(result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runGpgVerify", () => {
|
||||
it("should successfully verify valid signature", async (t) => {
|
||||
const execCalls = [];
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-success");
|
||||
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||
|
||||
assert.equal(execCalls.length, 1);
|
||||
assert.equal(execCalls[0].command, "gpg");
|
||||
assert.ok(execCalls[0].args.includes("--verify"));
|
||||
assert.ok(execCalls[0].args.includes(signaturePath));
|
||||
assert.ok(execCalls[0].args.includes(zipPath));
|
||||
});
|
||||
|
||||
it("should throw error when signature verification fails", async (t) => {
|
||||
const execFn = mock.fn(async () => {
|
||||
throw new Error("BAD signature");
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-fail");
|
||||
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await assert.rejects(
|
||||
() => runGpgVerify(zipPath, signaturePath, gpgHome),
|
||||
{
|
||||
message: /GPG signature verification failed - file may be corrupted or tampered/
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should convert Windows paths for GPG", async (t) => {
|
||||
const execCalls = [];
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-windows");
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: "win32",
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const zipPath = String.raw`C:\temp\scanner.zip`;
|
||||
const signaturePath = String.raw`C:\temp\scanner.zip.asc`;
|
||||
|
||||
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||
|
||||
// Verify paths were converted to Unix format
|
||||
const args = execCalls[0].args;
|
||||
const homeDirIndex = args.indexOf("--homedir");
|
||||
const zipIndex = args.indexOf("--verify") + 1;
|
||||
|
||||
// Check that Windows paths are converted (should start with /c/ instead of C:\)
|
||||
assert.ok(!args[homeDirIndex + 1].includes("\\"));
|
||||
assert.ok(!args[zipIndex].includes("\\"));
|
||||
assert.ok(!args[zipIndex + 1].includes("\\"));
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifySignature", () => {
|
||||
it("should successfully verify signature with GPG available", async (t) => {
|
||||
const execCalls = [];
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=full-verify-success");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await verifySignature(zipPath, signaturePath);
|
||||
|
||||
assert.equal(execCalls.length, 3);
|
||||
assert.deepEqual(execCalls[0].args, ["--version"]);
|
||||
assert.ok(execCalls[1].args.includes("--recv-keys"));
|
||||
assert.ok(execCalls[2].args.includes("--verify"));
|
||||
});
|
||||
|
||||
it("should throw error when GPG is not available", async (t) => {
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
if (args.includes("--version")) {
|
||||
throw new Error("GPG not found");
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=no-gpg");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await assert.rejects(
|
||||
() => verifySignature(zipPath, signaturePath),
|
||||
{
|
||||
message: /GPG is not available/
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error when signature verification fails", async (t) => {
|
||||
let callCount = 0;
|
||||
const execFn = mock.fn(async () => {
|
||||
callCount++;
|
||||
// First call: gpg --version (success)
|
||||
if (callCount === 1) {
|
||||
return 0;
|
||||
}
|
||||
// Second call: recv-keys (success)
|
||||
if (callCount === 2) {
|
||||
return 0;
|
||||
}
|
||||
// Third call: verify (failure - bad signature)
|
||||
throw new Error("BAD signature from 679F1EE92B19609DE816FDE81DB198F93525EC1A");
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=bad-signature");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await assert.rejects(
|
||||
() => verifySignature(zipPath, signaturePath),
|
||||
{
|
||||
message: /GPG signature verification failed - file may be corrupted or tampered/
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should cleanup GPG home directory even on failure", async (t) => {
|
||||
let createdGpgHome;
|
||||
let callCount = 0;
|
||||
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
callCount++;
|
||||
// First call: gpg --version (success)
|
||||
if (callCount === 1) {
|
||||
return 0;
|
||||
}
|
||||
// Second call: recv-keys (success)
|
||||
if (callCount === 2) {
|
||||
// Capture the GPG home directory from the args
|
||||
const homeDirIndex = args.indexOf("--homedir");
|
||||
if (homeDirIndex !== -1) {
|
||||
createdGpgHome = args[homeDirIndex + 1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Third call: verify (failure)
|
||||
throw new Error("BAD signature");
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=cleanup-on-fail");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await assert.rejects(
|
||||
() => verifySignature(zipPath, signaturePath),
|
||||
{
|
||||
message: /GPG signature verification failed/
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(!fs.existsSync(createdGpgHome), "GPG home should have been deleted after failure");
|
||||
});
|
||||
|
||||
it("should use custom keyserver and fingerprint when provided", async (t) => {
|
||||
const execCalls = [];
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=custom-options");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
const customKeyserver = "hkps://custom.keyserver.example.com";
|
||||
const customFingerprint = "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234";
|
||||
|
||||
await verifySignature(zipPath, signaturePath, {
|
||||
keyserver: customKeyserver,
|
||||
keyFingerprint: customFingerprint,
|
||||
});
|
||||
|
||||
const recvKeysCall = execCalls.find(call => call.args.includes("--recv-keys"));
|
||||
assert.ok(recvKeysCall, "Should have recv-keys call");
|
||||
assert.ok(recvKeysCall.args.includes(customKeyserver));
|
||||
assert.ok(recvKeysCall.args.includes(customFingerprint));
|
||||
});
|
||||
|
||||
it("should use default keyserver and fingerprint when not provided", async (t) => {
|
||||
const execCalls = [];
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { verifySignature } = await import("../gpg-verification.js?test=default-options");
|
||||
|
||||
const zipPath = "/tmp/scanner.zip";
|
||||
const signaturePath = "/tmp/scanner.zip.asc";
|
||||
|
||||
await verifySignature(zipPath, signaturePath);
|
||||
|
||||
const recvKeysCall = execCalls.find(call => call.args.includes("--recv-keys"));
|
||||
assert.ok(recvKeysCall, "Should have recv-keys call");
|
||||
assert.ok(recvKeysCall.args.includes("hkps://keyserver.ubuntu.com"));
|
||||
assert.ok(recvKeysCall.args.includes("679F1EE92B19609DE816FDE81DB198F93525EC1A"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("importSonarSourceKey", () => {
|
||||
it("should use fallback keyserver when primary fails", async (t) => {
|
||||
const execCalls = [];
|
||||
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
|
||||
const argsString = args.join(" ");
|
||||
if (argsString.includes("invalid.keyserver.that.does.not.exist.example.com")) {
|
||||
throw new Error("Failed to import key from invalid keyserver");
|
||||
}
|
||||
if (argsString.includes("keys.openpgp.org")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { importSonarSourceKey } = await import("../gpg-verification.js?test=fallback");
|
||||
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const invalidKeyserver = "hkps://invalid.keyserver.that.does.not.exist.example.com";
|
||||
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||
|
||||
await importSonarSourceKey(gpgHome, keyFingerprint, invalidKeyserver);
|
||||
|
||||
assert.equal(execCalls.length, 2, "Should attempt two keyservers");
|
||||
|
||||
// Verify primary keyserver call
|
||||
assert.equal(execCalls[0].command, "gpg");
|
||||
assert.ok(execCalls[0].args.includes(invalidKeyserver));
|
||||
assert.ok(execCalls[0].args.includes(keyFingerprint));
|
||||
|
||||
// Verify fallback keyserver call
|
||||
assert.equal(execCalls[1].command, "gpg");
|
||||
assert.ok(execCalls[1].args.includes("hkps://keys.openpgp.org"));
|
||||
assert.ok(execCalls[1].args.includes(keyFingerprint));
|
||||
});
|
||||
|
||||
it("should succeed with valid keyserver", async (t) => {
|
||||
const execCalls = [];
|
||||
|
||||
const execFn = mock.fn(async (command, args) => {
|
||||
execCalls.push({ command, args });
|
||||
return 0;
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { importSonarSourceKey } = await import("../gpg-verification.js?test=valid");
|
||||
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const keyserver = "hkps://keyserver.ubuntu.com";
|
||||
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||
|
||||
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
|
||||
|
||||
assert.equal(execCalls.length, 1);
|
||||
assert.equal(execCalls[0].command, "gpg");
|
||||
assert.ok(execCalls[0].args.includes(keyserver));
|
||||
assert.ok(execCalls[0].args.includes(keyFingerprint));
|
||||
assert.ok(execCalls[0].args.includes("--recv-keys"));
|
||||
});
|
||||
|
||||
it("should throw error when both keyservers fail", async (t) => {
|
||||
const execFn = mock.fn(async () => {
|
||||
throw new Error("Connection failed");
|
||||
});
|
||||
|
||||
t.mock.module("@actions/exec", {
|
||||
namedExports: {
|
||||
exec: execFn,
|
||||
},
|
||||
});
|
||||
|
||||
const { importSonarSourceKey } = await import("../gpg-verification.js?test=both-fail");
|
||||
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
const keyserver = "hkps://keyserver.ubuntu.com";
|
||||
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||
|
||||
await assert.rejects(
|
||||
() => importSonarSourceKey(gpgHome, keyFingerprint, keyserver),
|
||||
{
|
||||
message: /Failed to import SonarSource public key from all keyservers/
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* sonarqube-scan-action
|
||||
* Copyright (C) 2025 SonarSource SA
|
||||
* mailto:info AT sonarsource DOT com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import { describe, it, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
getGpgCommand,
|
||||
setupGpgHome,
|
||||
cleanupGpgHome,
|
||||
convertToUnixPath,
|
||||
} from "../gpg-verification.js";
|
||||
|
||||
/**
|
||||
* Helper function to temporarily mock process.platform for a test.
|
||||
* Automatically restores the original platform value after the test.
|
||||
* @param {string} platform - The platform to mock (e.g., "win32", "linux")
|
||||
* @param {Function} testFn - The test function to run with the mocked platform
|
||||
*/
|
||||
function withMockedPlatform(platform, testFn) {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: platform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
try {
|
||||
testFn();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a GPG home directory and track it for cleanup.
|
||||
* @param {Array} tempDirs - Array to track temporary directories for cleanup
|
||||
* @returns {string} The path to the created GPG home directory
|
||||
*/
|
||||
function createTrackedGpgHome(tempDirs) {
|
||||
const gpgHome = setupGpgHome();
|
||||
tempDirs.push(gpgHome);
|
||||
assert.ok(fs.existsSync(gpgHome));
|
||||
return gpgHome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to temporarily mock environment variables for a test.
|
||||
* Automatically restores or deletes environment variables after the test.
|
||||
* @param {Object} envVars - Object with environment variable names as keys and values as values
|
||||
* @param {Function} testFn - The async test function to run with the mocked environment
|
||||
*/
|
||||
async function withMockedEnv(envVars, testFn) {
|
||||
const originalValues = {};
|
||||
|
||||
// Save original values and set new ones
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
originalValues[key] = process.env[key];
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
await testFn();
|
||||
} finally {
|
||||
// Restore or delete environment variables
|
||||
for (const [key, originalValue] of Object.entries(originalValues)) {
|
||||
if (originalValue === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = originalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a temporary directory.
|
||||
* @returns {string} The path to the created temporary directory
|
||||
*/
|
||||
function createTempDir() {
|
||||
const tempDir = path.join(os.tmpdir(), `test-runner-temp-${Date.now()}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
describe("gpg-verification", () => {
|
||||
let tempDirs = [];
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up any temporary directories created during tests
|
||||
tempDirs.forEach((dir) => {
|
||||
try {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
describe("getGpgCommand", () => {
|
||||
it("should return 'gpg' as the command", () => {
|
||||
const command = getGpgCommand();
|
||||
assert.equal(command, "gpg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertToUnixPath", () => {
|
||||
it("should convert Windows path with drive letter to Unix path", () => {
|
||||
withMockedPlatform("win32", () => {
|
||||
assert.equal(
|
||||
convertToUnixPath(String.raw`C:\a\_temp\gpg-home`),
|
||||
"/c/a/_temp/gpg-home"
|
||||
);
|
||||
assert.equal(
|
||||
convertToUnixPath(String.raw`D:\Users\test\file.txt`),
|
||||
"/d/Users/test/file.txt"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle mixed slashes on Windows", () => {
|
||||
withMockedPlatform("win32", () => {
|
||||
assert.equal(
|
||||
convertToUnixPath(String.raw`C:\a/_temp\gpg-home`),
|
||||
"/c/a/_temp/gpg-home"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return path unchanged on non-Windows platforms", () => {
|
||||
withMockedPlatform("linux", () => {
|
||||
assert.equal(
|
||||
convertToUnixPath("/tmp/gpg-home"),
|
||||
"/tmp/gpg-home"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupGpgHome", () => {
|
||||
it("should create a temporary GPG home directory", () => {
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
|
||||
assert.ok(fs.statSync(gpgHome).isDirectory());
|
||||
|
||||
// Check directory permissions (on Unix systems)
|
||||
if (process.platform !== "win32") {
|
||||
const stats = fs.statSync(gpgHome);
|
||||
const mode = stats.mode & Number.parseInt("777", 8);
|
||||
assert.equal(mode, Number.parseInt("700", 8));
|
||||
}
|
||||
});
|
||||
|
||||
it("should create unique directories on multiple calls", async () => {
|
||||
const gpgHome1 = createTrackedGpgHome(tempDirs);
|
||||
// Small delay to ensure different timestamps
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const gpgHome2 = createTrackedGpgHome(tempDirs);
|
||||
|
||||
assert.notEqual(gpgHome1, gpgHome2);
|
||||
});
|
||||
|
||||
it("should use RUNNER_TEMP if available", async () => {
|
||||
const testTemp = createTempDir();
|
||||
|
||||
await withMockedEnv({ RUNNER_TEMP: testTemp }, async () => {
|
||||
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||
assert.ok(gpgHome.startsWith(testTemp));
|
||||
});
|
||||
|
||||
if (fs.existsSync(testTemp)) {
|
||||
fs.rmSync(testTemp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanupGpgHome", () => {
|
||||
it("should remove the GPG home directory", () => {
|
||||
const gpgHome = setupGpgHome();
|
||||
assert.ok(fs.existsSync(gpgHome));
|
||||
|
||||
cleanupGpgHome(gpgHome);
|
||||
assert.ok(!fs.existsSync(gpgHome));
|
||||
});
|
||||
|
||||
it("should not throw if directory does not exist", () => {
|
||||
const nonExistentDir = path.join(os.tmpdir(), `non-existent-${Date.now()}`);
|
||||
assert.doesNotThrow(() => {
|
||||
cleanupGpgHome(nonExistentDir);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle cleanup errors gracefully", () => {
|
||||
// This test verifies the function doesn't throw on permission errors
|
||||
// In practice, permission errors are rare in test environments
|
||||
assert.doesNotThrow(() => {
|
||||
cleanupGpgHome("/invalid/path/that/does/not/exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
export function mockCore(overrides = {}) {
|
||||
return {
|
||||
setFailed: (msg) => console.error(msg),
|
||||
warning: (msg) => console.log(msg),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import mockfs from "mock-fs";
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it, mock } from "node:test";
|
||||
import {
|
||||
checkGradleProject,
|
||||
checkMavenProject,
|
||||
checkSonarToken,
|
||||
validateScannerVersion,
|
||||
} from "../sanity-checks.js";
|
||||
import { mockCore } from "./mocks.js";
|
||||
|
||||
describe("validateScannerVersion", () => {
|
||||
const expected =
|
||||
"Invalid scannerVersion format. Expected format: x.y.z.w (e.g., 7.1.0.4889)";
|
||||
|
||||
const validVersions = [undefined, "", "7.1.0.4889", "1.2.3.4"];
|
||||
|
||||
const invalidVersions = [
|
||||
"wrong",
|
||||
"4.2.",
|
||||
"7.1.0",
|
||||
"7.1.0.abc",
|
||||
"7.1.0.4889.5",
|
||||
"7.1",
|
||||
"7",
|
||||
"7.1.0.",
|
||||
".7.1.0.4889",
|
||||
"7..1.0.4889",
|
||||
"7.1..0.4889",
|
||||
"7.1.0..4889",
|
||||
"a.b.c.d",
|
||||
"7.1.0.4889-SNAPSHOT",
|
||||
"v7.1.0.4889",
|
||||
"7.1.0.4889.0.0",
|
||||
"-7.1.0.4889",
|
||||
"7.-1.0.4889",
|
||||
"7.1.-0.4889",
|
||||
"7.1.0.-4889",
|
||||
"7.1.0.4889 ",
|
||||
" 7.1.0.4889",
|
||||
"7.1.0.4889\n",
|
||||
"7,1,0,4889",
|
||||
];
|
||||
|
||||
validVersions.forEach((version) => {
|
||||
it(`accepts ${version}`, () => {
|
||||
assert.equal(validateScannerVersion(version), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
invalidVersions.forEach((version) =>
|
||||
it(`throws for ${version}`, () => {
|
||||
assert.throws(
|
||||
() => validateScannerVersion(version),
|
||||
{
|
||||
message: expected,
|
||||
},
|
||||
`should have thrown for ${version}`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkSonarToken", () => {
|
||||
it("calls core.warning when SONAR_TOKEN is not set", () => {
|
||||
const warning = mock.fn();
|
||||
|
||||
checkSonarToken(mockCore({ warning }));
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Running this GitHub Action without SONAR_TOKEN is not recommended"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call core.warning when SONAR_TOKEN is set", () => {
|
||||
const warning = mock.fn();
|
||||
|
||||
checkSonarToken(mockCore({ warning }), "test-token");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkMavenProject", () => {
|
||||
it("calls core.warning when pom.xml exists", async () => {
|
||||
mockfs({ "/test/project/": { "pom.xml": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject({ warning }, "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Maven project detected. Sonar recommends running the 'org.sonarsource.scanner.maven:sonar-maven-plugin:sonar' goal during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("does not call core.warning when pom.xml does not exist", async () => {
|
||||
mockfs({ "/test/project/": {} });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("handles project base dir with trailing slash", async () => {
|
||||
mockfs({ "/test/project/": { "pom.xml": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkMavenProject(mockCore({ warning }), "/test/project/");
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkGradleProject", () => {
|
||||
it("calls core.warning when build.gradle exists", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle": "" } });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("calls core.warning when build.gradle.kts exists", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle.kts": "" } });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
warning.mock.calls[0].arguments[0],
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("does not call core.warning when neither gradle file exists", async () => {
|
||||
mockfs({ "/test/project/": {} });
|
||||
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 0);
|
||||
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
it("handles project base dir with trailing slash", async () => {
|
||||
mockfs({ "/test/project/": { "build.gradle": "" } });
|
||||
const warning = mock.fn();
|
||||
|
||||
checkGradleProject(mockCore({ warning }), "/test/project/");
|
||||
|
||||
assert.equal(warning.mock.calls.length, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
getPlatformFlavor,
|
||||
getScannerDownloadURL,
|
||||
scannerDirName,
|
||||
} from "../utils.js";
|
||||
|
||||
describe("getPlatformFlavor", () => {
|
||||
const supportedPlatforms = [
|
||||
{ platform: "linux", arch: "x64", expected: "linux-x64" },
|
||||
{ platform: "linux", arch: "arm64", expected: "linux-aarch64" },
|
||||
{ platform: "win32", arch: "x64", expected: "windows-x64" },
|
||||
{ platform: "darwin", arch: "x64", expected: "macosx-x64" },
|
||||
{ platform: "darwin", arch: "arm64", expected: "macosx-aarch64" },
|
||||
];
|
||||
|
||||
const unsupportedPlatforms = [
|
||||
{ platform: "linux", arch: "arm" },
|
||||
{ platform: "openbsd", arch: "x64" },
|
||||
{ platform: undefined, arch: "x64" },
|
||||
{ platform: "linux", arch: undefined },
|
||||
{ platform: null, arch: "x64" },
|
||||
{ platform: "linux", arch: null },
|
||||
];
|
||||
|
||||
supportedPlatforms.forEach(({ platform, arch, expected }) => {
|
||||
it(`returns ${expected} for ${platform} ${arch}`, () => {
|
||||
assert.equal(getPlatformFlavor(platform, arch), expected);
|
||||
});
|
||||
});
|
||||
|
||||
unsupportedPlatforms.forEach(({ platform, arch }) => {
|
||||
it(`throws for unsupported platform ${platform} ${arch}`, () => {
|
||||
assert.throws(
|
||||
() => getPlatformFlavor(platform, arch),
|
||||
{
|
||||
message: `Platform ${platform} ${arch} not supported`,
|
||||
},
|
||||
`should have thrown for ${platform} ${arch}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScannerDownloadURL", () => {
|
||||
it("generates correct URL without trailing slash", () => {
|
||||
const result = getScannerDownloadURL({
|
||||
scannerBinariesUrl:
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli",
|
||||
scannerVersion: "7.2.0.5079",
|
||||
flavor: "linux-x64",
|
||||
});
|
||||
assert.equal(
|
||||
result,
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-x64.zip"
|
||||
);
|
||||
});
|
||||
|
||||
it("generates correct URL with trailing slash", () => {
|
||||
const result = getScannerDownloadURL({
|
||||
scannerBinariesUrl:
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/",
|
||||
scannerVersion: "7.2.0.5079",
|
||||
flavor: "linux-x64",
|
||||
});
|
||||
assert.equal(
|
||||
result,
|
||||
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-7.2.0.5079-linux-x64.zip"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scannerDirName", () => {
|
||||
it("handles special characters", () => {
|
||||
assert.equal(
|
||||
scannerDirName("7.2.0-SNAPSHOT", "linux_x64"),
|
||||
"sonar-scanner-7.2.0-SNAPSHOT-linux_x64"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* sonarqube-scan-action
|
||||
* Copyright (C) 2025 SonarSource SA
|
||||
* mailto:info AT sonarsource DOT com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const SONARSOURCE_KEY_FINGERPRINT = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||
const DEFAULT_KEYSERVER = "hkps://keyserver.ubuntu.com";
|
||||
const FALLBACK_KEYSERVER = "hkps://keys.openpgp.org";
|
||||
|
||||
/**
|
||||
* Verifies the GPG signature of a downloaded file
|
||||
* @param {string} zipPath - Path to the downloaded ZIP file
|
||||
* @param {string} signaturePath - Path to the .asc signature file
|
||||
* @param {object} options - Verification options
|
||||
* @param {string} options.keyFingerprint - GPG key fingerprint (default: SonarSource key)
|
||||
* @param {string} options.keyserver - Primary keyserver URL (default: keyserver.ubuntu.com, with fallback to keys.openpgp.org)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If GPG is unavailable or verification fails
|
||||
*/
|
||||
export async function verifySignature(zipPath, signaturePath, options = {}) {
|
||||
const keyFingerprint = options.keyFingerprint || SONARSOURCE_KEY_FINGERPRINT;
|
||||
const keyserver = options.keyserver || DEFAULT_KEYSERVER;
|
||||
|
||||
if (!(await isGpgAvailable())) {
|
||||
throw new Error(
|
||||
"GPG is not available. Install GPG or set skipSignatureVerification: true"
|
||||
);
|
||||
}
|
||||
|
||||
let gpgHome;
|
||||
try {
|
||||
gpgHome = setupGpgHome();
|
||||
core.debug(`Created temporary GPG home: ${gpgHome}`);
|
||||
|
||||
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
|
||||
core.info("✓ SonarSource public key imported successfully");
|
||||
|
||||
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||
core.info("✓ GPG signature verification passed");
|
||||
} finally {
|
||||
if (gpgHome) {
|
||||
cleanupGpgHome(gpgHome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if GPG is available on the system
|
||||
* @returns {Promise<boolean>} True if GPG is available
|
||||
*/
|
||||
export async function isGpgAvailable() {
|
||||
try {
|
||||
const gpgCommand = getGpgCommand();
|
||||
await exec.exec(gpgCommand, ["--version"], {
|
||||
silent: true,
|
||||
ignoreReturnCode: false,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
core.debug(`GPG not available: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the GPG command for the current platform
|
||||
* @returns {string} GPG command name
|
||||
*/
|
||||
export function getGpgCommand() {
|
||||
// GPG is available as 'gpg' on all GitHub-hosted runners
|
||||
return "gpg";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Windows path to Unix-style path for GPG
|
||||
* GPG on Windows (from Git for Windows) expects Unix-style paths
|
||||
* @param {string} windowsPath - Windows path (e.g., C:\a\_temp\gpg-home)
|
||||
* @returns {string} Unix-style path (e.g., /c/a/_temp/gpg-home)
|
||||
*/
|
||||
export function convertToUnixPath(windowsPath) {
|
||||
if (process.platform !== "win32") {
|
||||
return windowsPath;
|
||||
}
|
||||
|
||||
let unixPath = windowsPath.replaceAll('\\', "/");
|
||||
|
||||
unixPath = unixPath.replace(/^([A-Za-z]):/, (match, drive) => {
|
||||
return `/${drive.toLowerCase()}`;
|
||||
});
|
||||
|
||||
return unixPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary GPG home directory
|
||||
* @returns {string} Path to the temporary GPG home directory
|
||||
*/
|
||||
export function setupGpgHome() {
|
||||
const tempDir = process.env.RUNNER_TEMP || os.tmpdir();
|
||||
const gpgHome = path.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
|
||||
|
||||
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
|
||||
|
||||
return gpgHome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to import a public key from a specific keyserver
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
* @param {string} keyFingerprint - Public key fingerprint
|
||||
* @param {string} keyserver - Keyserver URL
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If key import fails
|
||||
*/
|
||||
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||
const gpgCommand = getGpgCommand();
|
||||
const gpgHomePath = convertToUnixPath(gpgHome);
|
||||
|
||||
await exec.exec(
|
||||
gpgCommand,
|
||||
[
|
||||
"--homedir",
|
||||
gpgHomePath,
|
||||
"--batch",
|
||||
"--keyserver",
|
||||
keyserver,
|
||||
"--recv-keys",
|
||||
keyFingerprint,
|
||||
],
|
||||
{
|
||||
silent: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the SonarSource public key from a keyserver
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
* @param {string} keyFingerprint - Public key fingerprint
|
||||
* @param {string} keyserver - Keyserver URL
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If key import fails
|
||||
*/
|
||||
export async function importSonarSourceKey(gpgHome, keyFingerprint, keyserver) {
|
||||
let primaryError;
|
||||
|
||||
try {
|
||||
core.info(`Importing SonarSource public key from ${keyserver}...`);
|
||||
await tryImportKey(gpgHome, keyFingerprint, keyserver);
|
||||
core.info(`Successfully imported key from ${keyserver}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
primaryError = error;
|
||||
core.warning(
|
||||
`Failed to import key from ${keyserver}: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
core.info(`Attempting fallback keyserver ${FALLBACK_KEYSERVER}...`);
|
||||
await tryImportKey(gpgHome, keyFingerprint, FALLBACK_KEYSERVER);
|
||||
core.info(`Successfully imported key from fallback keyserver ${FALLBACK_KEYSERVER}`);
|
||||
} catch (fallbackError) {
|
||||
throw new Error(
|
||||
`Failed to import SonarSource public key from all keyservers. ` +
|
||||
`Primary (${keyserver}): ${primaryError.message}. ` +
|
||||
`Fallback (${FALLBACK_KEYSERVER}): ${fallbackError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs GPG verification on the downloaded file
|
||||
* @param {string} zipPath - Path to the ZIP file
|
||||
* @param {string} signaturePath - Path to the signature file
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If verification fails
|
||||
*/
|
||||
export async function runGpgVerify(zipPath, signaturePath, gpgHome) {
|
||||
const gpgCommand = getGpgCommand();
|
||||
|
||||
try {
|
||||
core.info("Verifying GPG signature...");
|
||||
await exec.exec(
|
||||
gpgCommand,
|
||||
[
|
||||
"--homedir",
|
||||
convertToUnixPath(gpgHome),
|
||||
"--batch",
|
||||
"--verify",
|
||||
convertToUnixPath(signaturePath),
|
||||
convertToUnixPath(zipPath),
|
||||
],
|
||||
{
|
||||
silent: false,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`GPG signature verification failed - file may be corrupted or tampered: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the temporary GPG home directory
|
||||
* @param {string} gpgHome - Path to GPG home directory
|
||||
*/
|
||||
export function cleanupGpgHome(gpgHome) {
|
||||
try {
|
||||
if (fs.existsSync(gpgHome)) {
|
||||
fs.rmSync(gpgHome, { recursive: true, force: true });
|
||||
core.debug(`Cleaned up temporary GPG home: ${gpgHome}`);
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(`Failed to cleanup temporary GPG home: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { installSonarScanner } from "./install-sonar-scanner";
|
||||
import { runSonarScanner } from "./run-sonar-scanner";
|
||||
import {
|
||||
checkGradleProject,
|
||||
checkMavenProject,
|
||||
checkSonarToken,
|
||||
validateScannerVersion,
|
||||
} from "./sanity-checks";
|
||||
|
||||
/**
|
||||
* Inputs are defined in action.yml
|
||||
*/
|
||||
function getInputs() {
|
||||
const args = core.getInput("args");
|
||||
const projectBaseDir = core.getInput("projectBaseDir");
|
||||
const scannerBinariesUrl = core.getInput("scannerBinariesUrl");
|
||||
const scannerVersion = core.getInput("scannerVersion");
|
||||
const skipSignatureVerification = core.getBooleanInput("skipSignatureVerification");
|
||||
|
||||
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion, skipSignatureVerification };
|
||||
}
|
||||
|
||||
/**
|
||||
* These RUNNER env variables come from GitHub by default.
|
||||
* See https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
|
||||
*
|
||||
* The others are optional env variables provided by the user of the action
|
||||
*/
|
||||
function getEnvVariables() {
|
||||
return {
|
||||
runnerDebug: process.env.RUNNER_DEBUG,
|
||||
runnerOs: process.env.RUNNER_OS,
|
||||
runnerTemp: process.env.RUNNER_TEMP,
|
||||
sonarRootCert: process.env.SONAR_ROOT_CERT,
|
||||
sonarcloudUrl: process.env.SONARCLOUD_URL,
|
||||
sonarToken: process.env.SONAR_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
function runSanityChecks(inputs) {
|
||||
try {
|
||||
const { projectBaseDir, scannerVersion, sonarToken } = inputs;
|
||||
|
||||
validateScannerVersion(scannerVersion);
|
||||
checkSonarToken(core, sonarToken);
|
||||
checkMavenProject(core, projectBaseDir);
|
||||
checkGradleProject(core, projectBaseDir);
|
||||
} catch (error) {
|
||||
core.setFailed(`Sanity checks failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } =
|
||||
getInputs();
|
||||
const runnerEnv = getEnvVariables();
|
||||
const { sonarToken } = runnerEnv;
|
||||
|
||||
runSanityChecks({ projectBaseDir, scannerVersion, sonarToken });
|
||||
|
||||
const scannerDir = await installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
skipSignatureVerification,
|
||||
});
|
||||
|
||||
await runSonarScanner(args, projectBaseDir, scannerDir, runnerEnv);
|
||||
} catch (error) {
|
||||
core.setFailed(`Action failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,98 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
getPlatformFlavor,
|
||||
getScannerDownloadURL,
|
||||
scannerDirName,
|
||||
} from "./utils";
|
||||
import { verifySignature } from "./gpg-verification";
|
||||
|
||||
const TOOLNAME = "sonar-scanner-cli";
|
||||
|
||||
/**
|
||||
* Download the Sonar Scanner CLI for the current environment and cache it.
|
||||
*/
|
||||
export async function installSonarScanner({
|
||||
scannerVersion,
|
||||
scannerBinariesUrl,
|
||||
skipSignatureVerification = false,
|
||||
}) {
|
||||
const flavor = getPlatformFlavor(os.platform(), os.arch());
|
||||
|
||||
// Check if tool is already cached
|
||||
let toolDir = tc.find(TOOLNAME, scannerVersion, flavor);
|
||||
|
||||
if (!toolDir) {
|
||||
core.info(
|
||||
`Installing Sonar Scanner CLI ${scannerVersion} for ${flavor}...`
|
||||
);
|
||||
|
||||
const downloadUrl = getScannerDownloadURL({
|
||||
scannerBinariesUrl,
|
||||
scannerVersion,
|
||||
flavor,
|
||||
});
|
||||
|
||||
core.info(`Downloading from: ${downloadUrl}`);
|
||||
|
||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||
|
||||
if (skipSignatureVerification) {
|
||||
core.warning("⚠ Skipping GPG signature verification (not recommended)");
|
||||
} else {
|
||||
const signatureUrl = `${downloadUrl}.asc`;
|
||||
core.info(`Downloading signature from: ${signatureUrl}`);
|
||||
|
||||
let signaturePath;
|
||||
try {
|
||||
signaturePath = await tc.downloadTool(signatureUrl);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to download signature file from ${signatureUrl}: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
await verifySignature(downloadPath, signaturePath);
|
||||
}
|
||||
|
||||
const extractedPath = await tc.extractZip(downloadPath);
|
||||
|
||||
// Find the actual scanner directory inside the extracted folder
|
||||
const scannerPath = path.join(
|
||||
extractedPath,
|
||||
scannerDirName(scannerVersion, flavor)
|
||||
);
|
||||
|
||||
toolDir = await tc.cacheDir(scannerPath, TOOLNAME, scannerVersion, flavor);
|
||||
|
||||
core.info(`Sonar Scanner CLI cached to: ${toolDir}`);
|
||||
} else {
|
||||
core.info(`Using cached Sonar Scanner CLI from: ${toolDir}`);
|
||||
}
|
||||
|
||||
// Add the bin directory to PATH
|
||||
const binDir = path.join(toolDir, "bin");
|
||||
core.addPath(binDir);
|
||||
|
||||
return toolDir;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { parseArgsStringToArgv } from "string-argv";
|
||||
|
||||
const KEYTOOL_MAIN_CLASS = "sun.security.tools.keytool.Main";
|
||||
const TRUSTSTORE_PASSWORD = "changeit"; // default password of the Java truststore!
|
||||
|
||||
export async function runSonarScanner(
|
||||
inputArgs,
|
||||
projectBaseDir,
|
||||
scannerDir,
|
||||
runnerEnv = {}
|
||||
) {
|
||||
const { runnerDebug, runnerOs, runnerTemp, sonarRootCert, sonarcloudUrl } =
|
||||
runnerEnv;
|
||||
|
||||
const scannerBin =
|
||||
runnerOs === "Windows" ? "sonar-scanner.bat" : "sonar-scanner";
|
||||
|
||||
const scannerArgs = [];
|
||||
|
||||
/**
|
||||
* Not sanitization is needed when populating scannerArgs.
|
||||
* @actions/exec will take care of sanitizing the args it receives.
|
||||
*/
|
||||
|
||||
if (sonarcloudUrl) {
|
||||
scannerArgs.push(`-Dsonar.scanner.sonarcloudUrl=${sonarcloudUrl}`);
|
||||
}
|
||||
|
||||
if (runnerDebug === "1") {
|
||||
scannerArgs.push("--debug");
|
||||
}
|
||||
|
||||
if (projectBaseDir) {
|
||||
scannerArgs.push(`-Dsonar.projectBaseDir=${projectBaseDir}`);
|
||||
}
|
||||
|
||||
// The SSL folder may exist on an uncleaned self-hosted runner
|
||||
const sslFolder = path.join(os.homedir(), ".sonar", "ssl");
|
||||
const truststoreFile = path.join(sslFolder, "truststore.p12");
|
||||
|
||||
const keytoolParams = {
|
||||
scannerDir,
|
||||
truststoreFile,
|
||||
};
|
||||
|
||||
if (fs.existsSync(truststoreFile)) {
|
||||
let aliasSonarIsPresent = true;
|
||||
|
||||
try {
|
||||
await checkSonarAliasInTruststore(keytoolParams);
|
||||
} catch (_) {
|
||||
aliasSonarIsPresent = false;
|
||||
core.info(
|
||||
`Existing Scanner truststore ${truststoreFile} does not contain 'sonar' alias`
|
||||
);
|
||||
}
|
||||
|
||||
if (aliasSonarIsPresent) {
|
||||
core.info(
|
||||
`Removing 'sonar' alias from already existing Scanner truststore: ${truststoreFile}`
|
||||
);
|
||||
await deleteSonarAliasFromTruststore(keytoolParams);
|
||||
}
|
||||
}
|
||||
|
||||
if (sonarRootCert) {
|
||||
core.info("Adding SSL certificate to the Scanner truststore");
|
||||
const tempCertPath = path.join(runnerTemp, "tmpcert.pem");
|
||||
|
||||
try {
|
||||
fs.unlinkSync(tempCertPath);
|
||||
} catch (_) {
|
||||
// File doesn't exist, ignore
|
||||
}
|
||||
|
||||
fs.writeFileSync(tempCertPath, sonarRootCert);
|
||||
fs.mkdirSync(sslFolder, { recursive: true });
|
||||
|
||||
await importCertificateToTruststore(keytoolParams, tempCertPath);
|
||||
|
||||
scannerArgs.push(
|
||||
`-Dsonar.scanner.truststorePassword=${TRUSTSTORE_PASSWORD}`
|
||||
);
|
||||
}
|
||||
|
||||
if (inputArgs) {
|
||||
/**
|
||||
* No sanitization, but it is parsing a string into an array of arguments in a safe way (= no command execution),
|
||||
* and with good enough support of quotes to support arguments containing spaces.
|
||||
*/
|
||||
const args = parseArgsStringToArgv(inputArgs);
|
||||
scannerArgs.push(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments are sanitized by `exec`
|
||||
*/
|
||||
await exec.exec(scannerBin, scannerArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use keytool for now, as SonarQube 10.6 and below doesn't support openssl generated keystores
|
||||
* keytool requires a password > 6 characters, so we won't use the default password 'sonar'
|
||||
*/
|
||||
function executeKeytoolCommand({
|
||||
scannerDir,
|
||||
truststoreFile,
|
||||
extraArgs,
|
||||
options = {},
|
||||
}) {
|
||||
const baseArgs = [
|
||||
KEYTOOL_MAIN_CLASS,
|
||||
"-storetype",
|
||||
"PKCS12",
|
||||
"-keystore",
|
||||
truststoreFile,
|
||||
"-storepass",
|
||||
TRUSTSTORE_PASSWORD,
|
||||
"-noprompt",
|
||||
"-trustcacerts",
|
||||
...extraArgs,
|
||||
];
|
||||
|
||||
return exec.exec(`${scannerDir}/jre/bin/java`, baseArgs, options);
|
||||
}
|
||||
|
||||
function importCertificateToTruststore(keytoolParams, certPath) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-importcert", "-alias", "sonar", "-file", certPath],
|
||||
});
|
||||
}
|
||||
|
||||
function checkSonarAliasInTruststore(keytoolParams) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-list", "-v", "-alias", "sonar"],
|
||||
options: { silent: true },
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSonarAliasFromTruststore(keytoolParams) {
|
||||
return executeKeytoolCommand({
|
||||
...keytoolParams,
|
||||
extraArgs: ["-delete", "-alias", "sonar"],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import fs from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export function validateScannerVersion(version) {
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const versionRegex = /^\d+\.\d+\.\d+\.\d+$/;
|
||||
if (!versionRegex.test(version)) {
|
||||
throw new Error(
|
||||
"Invalid scannerVersion format. Expected format: x.y.z.w (e.g., 7.1.0.4889)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkSonarToken(core, sonarToken) {
|
||||
if (!sonarToken) {
|
||||
core.warning(
|
||||
"Running this GitHub Action without SONAR_TOKEN is not recommended"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkMavenProject(core, projectBaseDir) {
|
||||
const pomPath = join(projectBaseDir.replace(/\/$/, ""), "pom.xml");
|
||||
if (fs.existsSync(pomPath)) {
|
||||
core.warning(
|
||||
"Maven project detected. Sonar recommends running the 'org.sonarsource.scanner.maven:sonar-maven-plugin:sonar' goal during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkGradleProject(core, projectBaseDir) {
|
||||
const baseDir = projectBaseDir.replace(/\/$/, "");
|
||||
const gradlePath = join(baseDir, "build.gradle");
|
||||
const gradleKtsPath = join(baseDir, "build.gradle.kts");
|
||||
|
||||
if (fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath)) {
|
||||
core.warning(
|
||||
"Gradle project detected. Sonar recommends using the SonarQube plugin for Gradle during the build process instead of using this GitHub Action to get more accurate results."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
const platformFlavor = {
|
||||
linux: {
|
||||
x64: "linux-x64",
|
||||
arm64: "linux-aarch64",
|
||||
},
|
||||
win32: {
|
||||
x64: "windows-x64",
|
||||
},
|
||||
darwin: {
|
||||
x64: "macosx-x64",
|
||||
arm64: "macosx-aarch64",
|
||||
},
|
||||
};
|
||||
|
||||
export function getPlatformFlavor(platform, arch) {
|
||||
const flavor = platformFlavor[platform]?.[arch];
|
||||
|
||||
if (!flavor) {
|
||||
throw new Error(`Platform ${platform} ${arch} not supported`);
|
||||
}
|
||||
|
||||
return flavor;
|
||||
}
|
||||
|
||||
export function getScannerDownloadURL({
|
||||
scannerBinariesUrl,
|
||||
scannerVersion,
|
||||
flavor,
|
||||
}) {
|
||||
const trimURL = scannerBinariesUrl.replace(/\/$/, "");
|
||||
return `${trimURL}/sonar-scanner-cli-${scannerVersion}-${flavor}.zip`;
|
||||
}
|
||||
|
||||
export const scannerDirName = (version, flavor) =>
|
||||
`sonar-scanner-${version}-${flavor}`;
|
||||
+19
-1
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
set -eou pipefail
|
||||
|
||||
error() { echo -e "\\e[31m✗ $*\\e[0m"; }
|
||||
@@ -11,4 +29,4 @@ $scriptDir/assertFileExists "$1"
|
||||
if ! grep -q "$2" "$1"; then
|
||||
error "'$2' not found in '$1'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
set -eou pipefail
|
||||
|
||||
error() { echo -e "\\e[31m✗ $*\\e[0m"; }
|
||||
@@ -7,4 +25,4 @@ error() { echo -e "\\e[31m✗ $*\\e[0m"; }
|
||||
if [ -f "$1" ]; then
|
||||
error "File '$1' found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
+19
-1
@@ -1,5 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SonarQube Scan Action
|
||||
# Copyright (C) SonarSource Sàrl
|
||||
# mailto:contact AT sonarsource DOT com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
set -eou pipefail
|
||||
|
||||
error() { echo -e "\\e[31m✗ $*\\e[0m"; }
|
||||
@@ -7,4 +25,4 @@ error() { echo -e "\\e[31m✗ $*\\e[0m"; }
|
||||
if [ ! -f "$1" ]; then
|
||||
error "File '$1' not found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// SonarQube Scan Action
|
||||
// Copyright (C) SonarSource Sàrl
|
||||
// mailto:contact AT sonarsource DOT com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
function main() {
|
||||
console.log("Hello World");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user