Files
craft-common-actions/.github/workflows/sonarcloud.yml
T
dependabot[bot] 9bcbea6fa2 ci(deps): bump taiki-e/install-action (#134)
Bumps the actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action).


Updates `taiki-e/install-action` from 2.88.1 to 2.89.1
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/68a389c788b2cd04016ce8c80ee23b80d2ab3d68...682ce6c3c696a4cd5168a05f98e51aa02b877941)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.89.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 09:13:33 -05:00

424 lines
20 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:
- 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:
- 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@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
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:
- 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@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
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:
- 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
# The instrumented workspace build overflows the ~14 GB free on a stock runner.
if: steps.token.outputs.available == 'true' && hashFiles('Cargo.toml') != '' && inputs.shards <= 1
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
- 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@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
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"
# Unpack outside the checkout: the scanner analyses everything under the workspace, and build
# output (generated sources) there would be counted as duplicated code.
extracted="$RUNNER_TEMP/coverage-archive"
mkdir -p "$extracted"
tar --zstd -xf archive/nextest-archive.tar.zst -C "$extracted"
# 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
"$llvm_bin/llvm-profdata" merge -sparse profraw/*.profraw -o coverage.profdata
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
- 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@4d1dc0c91ee400dcf8a6dddf2f705ac36542e304 # v8.4.0
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/**"