Files
craft-common-actions/.github/workflows/sonarcloud.yml
T
Dylan M. Taylor 2edf8b6175 ci: address review findings for Harden-Runner egress policy
- Add allowed-endpoints to ast-grep.yml matching test-ast-grep.yml
- Add pypi.org and files.pythonhosted.org to sonarcloud.yml for Python coverage
- Add packages.microsoft.com:443 and https mirrors to apt allowed-endpoints
- Set unvalidated reusable workflows (release, docker-build, gate, etc.) to audit mode
2026-11-10 08:26:20 -05:00

531 lines
24 KiB
YAML

name: SonarCloud
on:
workflow_call:
inputs:
project-key:
description: "SonarCloud project key (defaults to storytold_<repo>)"
type: string
required: false
default: ""
fonts-ref:
description: "craft-fonts commit for tests that lay out text (empty skips the checkout)"
type: string
required: false
default: ""
pdf-test-fonts:
description: "Provision the caller's pinned PDF test fonts with scripts/fetch-pdf-test-fonts.py"
type: boolean
required: false
default: false
python-coverage-source:
description: "Optional Python source directory to measure; requires python-coverage-script"
type: string
required: false
default: ""
python-coverage-script:
description: "Optional Python test script; requires python-coverage-source"
type: string
required: false
default: ""
timeout-minutes:
description: "Maximum job runtime, including coverage generation and the scan"
type: number
required: false
default: 90
shards:
description: >-
Run the Rust tests as this many parallel partitions of one prebuilt instrumented
archive (1 keeps the single `cargo llvm-cov` job; default 4). Doctests are not run when sharded.
type: number
required: false
default: 4
secrets:
SONAR_TOKEN:
description: "Legacy or repository-level SonarCloud analysis token"
required: false
SONAR_CLOUD_PAT:
description: "Organization-level SonarCloud personal access token"
required: false
permissions:
contents: read
env:
# Vendored code and xtask, plus the test code cargo llvm-cov leaves out on its own (tests/,
# examples/, benches/, tests.rs, *_tests.rs): the sharded merge calls llvm-cov export directly,
# which would otherwise report that test code as covered source.
COVERAGE_IGNORE: '(^|/)(vendor|xtask|tests|examples|benches)/|/(tests\.rs|[0-9a-zA-Z_-]+[_-]tests\.rs)$'
jobs:
plan:
name: Plan coverage shards
if: inputs.shards > 1
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
archive.ubuntu.com:80
azure.archive.ubuntu.com:80
azure.archive.ubuntu.com:443
packages.microsoft.com:443
binaries.sonarsource.com:443
crates.io:443
files.pythonhosted.org:443
github.com:443
index.crates.io:443
objects.githubusercontent.com:443
pypi.org:443
release-assets.githubusercontent.com:443
scanner.sonarcloud.io:443
security.ubuntu.com:80
security.ubuntu.com:443
sonarcloud.io:443
static.crates.io:443
static.rust-lang.org:443
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build shard matrix
id: matrix
env:
SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_PAT || secrets.SONAR_TOKEN }}
SHARDS: ${{ inputs.shards }}
run: |
# The matrix caps at 256 jobs, and nextest partitions need whole numbers.
case "$SHARDS" in
''|*[!0-9]*) echo "::error::shards must be a whole number, got '$SHARDS'"; exit 1 ;;
esac
if [ "$SHARDS" -gt 256 ]; then
echo "::error::shards must be at most 256 (the GitHub Actions matrix limit), got $SHARDS"
exit 1
fi
# Same gate as the scan job: no token or no Rust workspace means no coverage to shard.
if [ -n "$SONAR_TOKEN" ] && [ -f Cargo.toml ]; then
echo "matrix={\"shard\":[$(seq -s, 1 "$SHARDS")]}" >>"$GITHUB_OUTPUT"
else
echo 'matrix={"shard":[]}' >>"$GITHUB_OUTPUT"
fi
build:
name: Build instrumented test archive
needs: plan
if: needs.plan.outputs.matrix != '{"shard":[]}'
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
archive.ubuntu.com:80
azure.archive.ubuntu.com:80
azure.archive.ubuntu.com:443
packages.microsoft.com:443
binaries.sonarsource.com:443
crates.io:443
files.pythonhosted.org:443
github.com:443
index.crates.io:443
objects.githubusercontent.com:443
pypi.org:443
release-assets.githubusercontent.com:443
scanner.sonarcloud.io:443
security.ubuntu.com:80
security.ubuntu.com:443
sonarcloud.io:443
static.crates.io:443
static.rust-lang.org:443
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out craft-fonts
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.fonts-ref }}
- name: Require requested craft-fonts
if: inputs.fonts-ref != ''
shell: bash
env:
CRAFT_FONTS_PATH: ${{ github.workspace }}/craft-fonts
run: |
echo "CRAFT_FONTS_DIR=$CRAFT_FONTS_PATH" >> "$GITHUB_ENV"
echo "CRAFT_FONTS_REQUIRED=1" >> "$GITHUB_ENV"
- name: Free disk space
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
- name: Install Linux build dependencies
uses: storytold/craft-common-actions/actions/install-apt-packages@main
with:
packages: >-
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcursor-dev libxrandr-dev
libxi-dev libgtk-3-dev libfontconfig1-dev libfreetype-dev libasound2-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev
libgl1-mesa-dev mesa-vulkan-drivers
- name: Install Rust and cargo-llvm-cov
run: |
rustup toolchain install stable --profile minimal --component llvm-tools-preview
rustup default stable
- uses: taiki-e/install-action@b80e2bb2635f28eb98b9fce1e2f0887e1535c6b2 # v2.90.0
with:
tool: cargo-llvm-cov,cargo-nextest@0.9.150
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Provision pinned PDF test fonts
if: inputs.pdf-test-fonts
shell: bash
run: |
python3 scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts"
echo "CRAFT_PDF_TEST_FONT_DIR=$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV"
- name: Build instrumented test archive
env:
CARGO_TERM_COLOR: always
# Coverage comes from instrumentation, not DWARF; debuginfo is most of target/.
CARGO_PROFILE_DEV_DEBUG: "0"
run: |
# show-env sets the instrumentation flags the same way the unsharded run gets them.
# shellcheck disable=SC1090 # generated by cargo-llvm-cov at runtime
source <(cargo llvm-cov show-env --sh)
cargo nextest archive --workspace --archive-file nextest-archive.tar.zst
- name: Record the toolchain that built the archive
run: |
rustc -vV | sed -n 's/^release: //p' > rustc-release.txt
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: nextest-archive
path: |
nextest-archive.tar.zst
rustc-release.txt
retention-days: 1
coverage:
name: Tests ${{ matrix.shard }}/${{ inputs.shards }}
needs: [plan, build]
if: needs.plan.outputs.matrix != '{"shard":[]}'
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
archive.ubuntu.com:80
azure.archive.ubuntu.com:80
azure.archive.ubuntu.com:443
packages.microsoft.com:443
binaries.sonarsource.com:443
crates.io:443
files.pythonhosted.org:443
github.com:443
index.crates.io:443
objects.githubusercontent.com:443
pypi.org:443
release-assets.githubusercontent.com:443
scanner.sonarcloud.io:443
security.ubuntu.com:80
security.ubuntu.com:443
sonarcloud.io:443
static.crates.io:443
static.rust-lang.org:443
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out craft-fonts
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.fonts-ref }}
- name: Require requested craft-fonts
if: inputs.fonts-ref != ''
shell: bash
env:
CRAFT_FONTS_PATH: ${{ github.workspace }}/craft-fonts
run: |
echo "CRAFT_FONTS_DIR=$CRAFT_FONTS_PATH" >> "$GITHUB_ENV"
echo "CRAFT_FONTS_REQUIRED=1" >> "$GITHUB_ENV"
- name: Install Linux build dependencies
uses: storytold/craft-common-actions/actions/install-apt-packages@main
with:
packages: >-
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcursor-dev libxrandr-dev
libxi-dev libgtk-3-dev libfontconfig1-dev libfreetype-dev libasound2-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev
libgl1-mesa-dev mesa-vulkan-drivers
- uses: taiki-e/install-action@b80e2bb2635f28eb98b9fce1e2f0887e1535c6b2 # v2.90.0
with:
tool: cargo-nextest@0.9.150
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: nextest-archive
path: archive
- name: Provision pinned PDF test fonts
if: inputs.pdf-test-fonts
shell: bash
run: |
python3 scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts"
echo "CRAFT_PDF_TEST_FONT_DIR=$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV"
- name: Run this shard's tests
env:
# Each instrumented binary writes its profile here; the scan job merges them.
LLVM_PROFILE_FILE: ${{ github.workspace }}/profraw/shard-${{ matrix.shard }}-%p-%m.profraw
run: |
mkdir -p profraw
# nextest exits 100 when tests fail. A red test must not hide the static analysis, so
# that alone keeps the job green. Any other exit means the shard could not run its
# tests at all (for example a test binary nextest cannot list): fail the job, because a
# shard that runs nothing would scan an empty coverage report.
rc=0
cargo nextest run --archive-file archive/nextest-archive.tar.zst \
--partition count:${{ matrix.shard }}/${{ inputs.shards }} --no-fail-fast || rc=$?
if [ "$rc" -ne 0 ] && [ "$rc" -ne 100 ]; then
echo "::error::nextest could not run shard ${{ matrix.shard }} (exit $rc)"
exit "$rc"
fi
if ! ls profraw/*.profraw >/dev/null 2>&1; then
echo "::error::shard ${{ matrix.shard }} produced no coverage profiles"
exit 1
fi
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: profraw-${{ matrix.shard }}
path: profraw/
if-no-files-found: ignore
retention-days: 1
sonarcloud:
name: SonarCloud Scan
needs: [plan, build, coverage]
# Scan unless a prerequisite failed outright; a red test shard still lets the scan measure what ran.
if: ${{ !cancelled() && contains(fromJSON('["success","skipped"]'), needs.plan.result) && contains(fromJSON('["success","skipped"]'), needs.build.result) && contains(fromJSON('["success","skipped"]'), needs.coverage.result) }}
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
archive.ubuntu.com:80
azure.archive.ubuntu.com:80
azure.archive.ubuntu.com:443
packages.microsoft.com:443
binaries.sonarsource.com:443
crates.io:443
files.pythonhosted.org:443
github.com:443
index.crates.io:443
objects.githubusercontent.com:443
pypi.org:443
release-assets.githubusercontent.com:443
scanner.sonarcloud.io:443
security.ubuntu.com:80
security.ubuntu.com:443
sonarcloud.io:443
static.crates.io:443
static.rust-lang.org:443
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Check for SonarCloud token
id: token
env:
SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_PAT || secrets.SONAR_TOKEN }}
run: |
if [ -n "$SONAR_TOKEN" ]; then
echo "available=true" >>"$GITHUB_OUTPUT"
else
echo "available=false" >>"$GITHUB_OUTPUT"
echo "::notice::SONAR_TOKEN is not available (e.g. a fork pull request); skipping SonarCloud scan."
fi
- name: Validate SonarCloud project key
id: project
if: steps.token.outputs.available == 'true'
shell: bash
env:
PROJECT_KEY: ${{ inputs.project-key != '' && inputs.project-key || format('storytold_{0}', github.event.repository.name) }}
LC_ALL: C
run: |
# The scan action splits args; reject characters that could introduce properties.
if [[ ! "$PROJECT_KEY" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.:-]*$ || ${#PROJECT_KEY} -gt 400 ]]; then
echo "::error::Invalid SonarCloud project key: must start with an alphanumeric character or underscore and contain only letters, digits, dash, underscore, period or colon (max 400 characters)."
exit 1
fi
printf 'key=%s\n' "$PROJECT_KEY" >>"$GITHUB_OUTPUT"
- name: Check out craft-fonts
if: steps.token.outputs.available == 'true' && inputs.shards <= 1
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.fonts-ref }}
- name: Require requested craft-fonts
if: inputs.fonts-ref != '' && steps.token.outputs.available == 'true' && inputs.shards <= 1
shell: bash
env:
CRAFT_FONTS_PATH: ${{ github.workspace }}/craft-fonts
run: |
echo "CRAFT_FONTS_DIR=$CRAFT_FONTS_PATH" >> "$GITHUB_ENV"
echo "CRAFT_FONTS_REQUIRED=1" >> "$GITHUB_ENV"
- name: Free disk space
# Both a local coverage build and downloaded shard profiles plus the extracted test
# archive can overflow the free space on a stock runner. Clean before either path.
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != ''
run: |
df -h . "$RUNNER_TEMP"
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
df -h . "$RUNNER_TEMP"
- name: Install Linux build dependencies
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
uses: storytold/craft-common-actions/actions/install-apt-packages@main
with:
packages: >-
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcursor-dev libxrandr-dev
libxi-dev libgtk-3-dev libfontconfig1-dev libfreetype-dev libasound2-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev
libgl1-mesa-dev mesa-vulkan-drivers
- name: Install Rust and cargo-llvm-cov
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
run: |
rustup toolchain install stable --profile minimal --component llvm-tools-preview
rustup default stable
- uses: taiki-e/install-action@b80e2bb2635f28eb98b9fce1e2f0887e1535c6b2 # v2.90.0
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
with:
tool: cargo-llvm-cov
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Provision pinned PDF test fonts
if: inputs.pdf-test-fonts && steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
shell: bash
run: |
python3 scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts"
echo "CRAFT_PDF_TEST_FONT_DIR=$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV"
- name: Generate Rust coverage
id: coverage
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
# A red test must not hide the static analysis, so keep going and scan what was measured.
continue-on-error: true
env:
CARGO_TERM_COLOR: always
# Coverage comes from instrumentation, not DWARF; debuginfo is most of target/.
CARGO_PROFILE_DEV_DEBUG: "0"
run: >
cargo llvm-cov --workspace --ignore-run-fail --lcov --output-path lcov.info
--ignore-filename-regex "$COVERAGE_IGNORE"
- name: Download shard profiles and archive
if: steps.token.outputs.available == 'true' && inputs.shards > 1 && hashFiles('Cargo.toml') != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: profraw-*
path: profraw
merge-multiple: true
- name: Download test archive
if: steps.token.outputs.available == 'true' && inputs.shards > 1 && hashFiles('Cargo.toml') != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: nextest-archive
path: archive
- name: Install LLVM tools for the archive's toolchain
if: steps.token.outputs.available == 'true' && inputs.shards > 1 && hashFiles('Cargo.toml') != ''
run: rustup toolchain install "$(cat archive/rustc-release.txt)" --profile minimal --component llvm-tools-preview
- name: Combine shard coverage
if: steps.token.outputs.available == 'true' && inputs.shards > 1 && hashFiles('Cargo.toml') != ''
shell: bash
run: |
set -euo pipefail
toolchain="$(cat archive/rustc-release.txt)"
llvm_bin="$(rustup run "$toolchain" rustc --print sysroot)/lib/rustlib/$(rustup run "$toolchain" rustc -vV | sed -n 's/^host: //p')/bin"
df -h . "$RUNNER_TEMP"
# Do not hold every raw profile beside the expanded archive. Retain the raw inputs
# until the merge succeeds, then release them before unpacking the test binaries.
"$llvm_bin/llvm-profdata" merge -sparse profraw/*.profraw -o coverage.profdata
rm -f profraw/*.profraw
df -h . "$RUNNER_TEMP"
# Unpack outside the checkout: the scanner analyses everything under the workspace, and build
# output (generated sources) there would be counted as duplicated code.
extracted="$(mktemp -d "$RUNNER_TEMP/coverage-archive.XXXXXX")"
trap 'rm -rf "$extracted"' EXIT
tar --zstd -xf archive/nextest-archive.tar.zst -C "$extracted"
rm -f archive/nextest-archive.tar.zst
# Only test binaries run on the target; helpers and proc-macros are not instrumented profile sources.
objects=()
while IFS= read -r name; do
while IFS= read -r -d '' file; do objects+=("$file"); done < <(find "$extracted" -type f -name "$name" -print0)
done < <(jq -r '."rust-binaries"[] | select(."build-platform" == "target") | ."binary-path" | split("/") | last' "$extracted/target/nextest/binaries-metadata.json")
if [ "${#objects[@]}" -eq 0 ]; then
echo "::error::No test binaries found in the archive metadata"
exit 1
fi
extra=()
for object in "${objects[@]:1}"; do extra+=(-object "$object"); done
"$llvm_bin/llvm-cov" export -format=lcov -instr-profile=coverage.profdata \
"${objects[0]}" "${extra[@]}" -ignore-filename-regex="$COVERAGE_IGNORE" >lcov.info
rm -f coverage.profdata
- name: Generate Python coverage
if: steps.token.outputs.available == 'true' && (inputs.python-coverage-source != '' || inputs.python-coverage-script != '')
shell: bash
env:
PYTHON_COVERAGE_SOURCE: ${{ inputs.python-coverage-source }}
PYTHON_COVERAGE_SCRIPT: ${{ inputs.python-coverage-script }}
run: |
set -euo pipefail
if [[ -z "$PYTHON_COVERAGE_SOURCE" || -z "$PYTHON_COVERAGE_SCRIPT" ]]; then
echo '::error::Python coverage requires both source and test script inputs'
exit 1
fi
if [[ ! -d "$PYTHON_COVERAGE_SOURCE" || ! -f "$PYTHON_COVERAGE_SCRIPT" ]]; then
echo '::error::Python coverage source directory or test script is missing'
exit 1
fi
source_dir=$(realpath -- "$PYTHON_COVERAGE_SOURCE")
test_script=$(realpath -- "$PYTHON_COVERAGE_SCRIPT")
workspace_dir=$(pwd -P)
case "$source_dir" in
"$workspace_dir"|"$workspace_dir"/*) ;;
*) echo '::error::Python coverage source must be inside the checkout'; exit 1 ;;
esac
case "$test_script" in
"$workspace_dir"/*) ;;
*) echo '::error::Python test script must be inside the checkout'; exit 1 ;;
esac
case "$source_dir$test_script" in
*','*|*'['*|*']'*|*'*'*|*'?'*) echo '::error::Python coverage paths must not contain commas or glob characters'; exit 1 ;;
esac
coverage_env=$(mktemp -d "$RUNNER_TEMP/python-coverage.XXXXXX")
python3 -m venv "$coverage_env"
"$coverage_env/bin/python" -m pip install --disable-pip-version-check coverage==7.10.7
export COVERAGE_FILE="$coverage_env/.coverage"
export PYTHON_COVERAGE_SOURCE_REAL="$source_dir"
export PYTHON_COVERAGE_SCRIPT_REAL="$test_script"
coverage_config="$coverage_env/coverage.ini"
# Environment substitution keeps paths literal and shares selection with reporting.
printf '%s\n' '[run]' 'branch = true' 'source = ${PYTHON_COVERAGE_SOURCE_REAL}' 'omit = ${PYTHON_COVERAGE_SCRIPT_REAL}' > "$coverage_config"
status=0
"$coverage_env/bin/python" -m coverage run --rcfile="$coverage_config" "$test_script" || status=$?
"$coverage_env/bin/python" -m coverage xml --rcfile="$coverage_config" -o python-coverage.xml
exit "$status"
- name: SonarCloud Scan
if: steps.token.outputs.available == 'true'
uses: SonarSource/sonarqube-scan-action@e805d875275f728fac1885b413934be6d7c63f65 # v8.4.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_PAT || secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sonarcloud.io
with:
args: >
"-Dsonar.projectKey=${{ steps.project.outputs.key }}"
-Dsonar.organization=storytold
-Dsonar.rust.lcov.reportPaths=lcov.info
"-Dsonar.cpd.exclusions=**/i18n/**"