name: CI on: pull_request: push: branches: - main permissions: contents: read concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_INPUT: "1" UV_VERSION: "0.12.4" jobs: package: name: Package runs-on: ubuntu-latest timeout-minutes: 15 env: BUILD_TOOLS: /tmp/obliteratus-ci-tools steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install build tooling run: | python -m pip install "uv==${UV_VERSION}" UV_PROJECT_ENVIRONMENT="$BUILD_TOOLS" uv sync --locked --only-group ci - name: Build source and wheel distributions run: >- "$BUILD_TOOLS/bin/python" -m build --sdist --wheel - name: Verify wheel contents and entry point run: | python - <<'PY' from pathlib import Path from zipfile import ZipFile wheels = list(Path("dist").glob("*.whl")) if len(wheels) != 1: raise SystemExit(f"expected one wheel, found: {wheels}") with ZipFile(wheels[0]) as archive: names = set(archive.namelist()) required = { "app.py", "obliteratus/__init__.py", "obliteratus/local_ui.py", } missing = sorted(required - names) if missing: raise SystemExit(f"wheel is missing required modules: {missing}") entry_points = [ name for name in names if name.endswith(".dist-info/entry_points.txt") ] if len(entry_points) != 1: raise SystemExit(f"expected one entry_points.txt, found: {entry_points}") contents = archive.read(entry_points[0]).decode("utf-8") if "obliteratus = obliteratus.cli:main" not in contents: raise SystemExit("wheel is missing the obliteratus console entry point") print(f"verified wheel contents: {wheels[0]}") PY - name: Validate distribution metadata run: | mkdir -p package-evidence "$BUILD_TOOLS/bin/python" -m twine check dist/* | tee package-evidence/twine-check.txt sha256sum dist/* | tee package-evidence/SHA256SUMS - name: Verify installed wheel contract run: | mapfile -t wheels < <(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f -name '*.whl' -print) if [ "${#wheels[@]}" -ne 1 ]; then echo "expected exactly one wheel, found ${#wheels[@]}" exit 1 fi wheel_env="$RUNNER_TEMP/obliteratus-wheel-env" wheel_cwd="$RUNNER_TEMP/obliteratus-wheel-cwd" python -m venv "$wheel_env" mkdir -p "$wheel_cwd" "$wheel_env/bin/python" -m pip install --no-cache-dir "rich==15.0.0" "$wheel_env/bin/python" -m pip install --no-cache-dir --no-deps "${wheels[0]}" cd "$wheel_cwd" "$wheel_env/bin/python" -I - <<'PY' | tee "$GITHUB_WORKSPACE/package-evidence/wheel-import.txt" import importlib.metadata from pathlib import Path import obliteratus origin = Path(obliteratus.__file__).resolve() assert "site-packages" in origin.parts, origin assert obliteratus.__version__ == importlib.metadata.version("obliteratus") print(f"installed wheel import: {origin}") print(f"version: {obliteratus.__version__}") PY "$wheel_env/bin/python" -I -m obliteratus --help > "$GITHUB_WORKSPACE/package-evidence/wheel-module-help.txt" "$wheel_env/bin/obliteratus" --help > "$GITHUB_WORKSPACE/package-evidence/wheel-console-help.txt" - name: Verify installed sdist contract run: | mapfile -t sdists < <(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f -name '*.tar.gz' -print) if [ "${#sdists[@]}" -ne 1 ]; then echo "expected exactly one sdist, found ${#sdists[@]}" exit 1 fi sdist_env="$RUNNER_TEMP/obliteratus-sdist-env" sdist_cwd="$RUNNER_TEMP/obliteratus-sdist-cwd" python -m venv "$sdist_env" mkdir -p "$sdist_cwd" "$sdist_env/bin/python" -m pip install --no-cache-dir "rich==15.0.0" "$sdist_env/bin/python" -m pip install --no-cache-dir --no-deps "${sdists[0]}" cd "$sdist_cwd" "$sdist_env/bin/python" -I - <<'PY' | tee "$GITHUB_WORKSPACE/package-evidence/sdist-import.txt" import importlib.metadata from pathlib import Path import obliteratus origin = Path(obliteratus.__file__).resolve() assert "site-packages" in origin.parts, origin assert obliteratus.__version__ == importlib.metadata.version("obliteratus") print(f"installed sdist import: {origin}") print(f"version: {obliteratus.__version__}") PY "$sdist_env/bin/python" -I -m obliteratus --help > "$GITHUB_WORKSPACE/package-evidence/sdist-module-help.txt" "$sdist_env/bin/obliteratus" --help > "$GITHUB_WORKSPACE/package-evidence/sdist-console-help.txt" - name: Upload distributions and package evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: distributions-py3.12 path: | dist/ package-evidence/ if-no-files-found: error retention-days: 14 lint: name: Ruff runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install Ruff run: python -m pip install "ruff==0.16.2" - name: Install actionlint with checksum verification env: ACTIONLINT_VERSION: "1.7.12" ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" run: | archive="$RUNNER_TEMP/actionlint.tar.gz" curl -fsSLo "$archive" \ "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum -c - tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint - name: Validate GitHub Actions workflows run: | "$RUNNER_TEMP/actionlint" -no-color - name: Enforce Ruff F gate run: >- python -m ruff check --select F app.py obliteratus tests scripts/check_coverage_thresholds.py scripts/check_mutation_score.py scripts/check_quality_policy.py scripts/check_mutation_targets.py scripts/prepare_mutation_coverage.py scripts/run_prepared_mutmut.py scripts/mutmut_coverage_sitecustomize/sitecustomize.py scripts/check_conditional_policy.py scripts/check_test_risk_map.py scripts/conditional_gate_summary.py scripts/run_conditional_gate.py scripts/run_repeat_gate.py scripts/write_test_evidence.py scripts/check_supply_chain_policy.py scripts/gemma4_12b_recursive_loop.py - name: Report E501 legacy baseline if: always() run: >- python -m ruff check --select E501 --statistics app.py obliteratus tests scripts/check_coverage_thresholds.py scripts/check_mutation_score.py scripts/check_quality_policy.py scripts/check_mutation_targets.py scripts/prepare_mutation_coverage.py scripts/run_prepared_mutmut.py scripts/mutmut_coverage_sitecustomize/sitecustomize.py scripts/check_conditional_policy.py scripts/check_test_risk_map.py scripts/conditional_gate_summary.py scripts/run_conditional_gate.py scripts/run_repeat_gate.py scripts/write_test_evidence.py scripts/check_supply_chain_policy.py scripts/gemma4_12b_recursive_loop.py || true test: name: Tests py${{ matrix.python-version }} runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" env: CUDA_VISIBLE_DEVICES: "" HF_DATASETS_OFFLINE: "1" HF_HUB_DISABLE_TELEMETRY: "1" HF_HUB_OFFLINE: "1" TOKENIZERS_PARALLELISM: "false" TRANSFORMERS_OFFLINE: "1" TEST_ENV: /tmp/obliteratus-test-env steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install locked package and test tools run: | python -m pip install "uv==${UV_VERSION}" UV_PROJECT_ENVIRONMENT="$TEST_ENV" \ uv sync --locked --no-default-groups --extra dev --no-editable - name: Smoke import and CLI run: | "$TEST_ENV/bin/python" - <<'PY' import obliteratus version = getattr(obliteratus, "__version__", None) if version is not None: print(f"obliteratus version: {version}") else: print("obliteratus import: ok") PY "$TEST_ENV/bin/python" -m obliteratus --help - name: Run tests with coverage run: | mkdir -p test-results "$TEST_ENV/bin/python" -m pytest \ -m "not slow and not gpu and not mps and not mlx and not network and not download and not remote and not operator_ui" \ --cov-branch \ --cov-fail-under=0 \ --junitxml="test-results/junit-py${{ matrix.python-version }}.xml" \ --cov-report="xml:test-results/coverage-py${{ matrix.python-version }}.xml" \ --cov-report="json:test-results/coverage-py${{ matrix.python-version }}.json" - name: Generate exact-base coverage for module regression comparison if: matrix.python-version == '3.12' env: BASE_TEST_ENV: /tmp/obliteratus-base-test-env BASE_WORKTREE: /tmp/obliteratus-base-worktree COVERAGE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} run: | git rev-parse --verify "${COVERAGE_BASE}^{commit}" git worktree add --detach "$BASE_WORKTREE" "$COVERAGE_BASE" ( cd "$BASE_WORKTREE" UV_PROJECT_ENVIRONMENT="$BASE_TEST_ENV" \ uv sync --locked --no-default-groups --extra dev --no-editable COVERAGE_FILE="$RUNNER_TEMP/.coverage-base" \ "$BASE_TEST_ENV/bin/python" -m pytest \ -m "not slow and not gpu and not mps and not mlx and not network and not download and not remote and not operator_ui" \ --cov-branch \ --cov-fail-under=0 \ --cov-report="json:$GITHUB_WORKSPACE/test-results/base-coverage-py3.12.json" ) | tee test-results/base-tests-py3.12.log - name: Enforce line and branch coverage floors env: COVERAGE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} run: | module_args=() if [ "${{ matrix.python-version }}" = "3.12" ]; then module_args=( --base-report test-results/base-coverage-py3.12.json --touched-module-no-regression --new-module-min-line 80 --new-module-min-branch 75 ) fi "$TEST_ENV/bin/python" scripts/check_coverage_thresholds.py \ "test-results/coverage-py${{ matrix.python-version }}.json" \ --min-line 75 \ --min-branch 60 \ --min-file obliteratus/device.py=70 \ --min-file obliteratus/models/loader.py=70 \ --min-file obliteratus/architecture_profiles.py=70 \ --min-file obliteratus/cli.py=70 \ --min-file obliteratus/mlx_backend.py=70 \ --min-file obliteratus/evaluation/metrics.py=70 \ --min-file obliteratus/evaluation/advanced_metrics.py=70 \ --min-file obliteratus/reporting/report.py=70 \ --min-file obliteratus/community.py=70 \ --min-file obliteratus/telemetry.py=70 \ --min-changed 95 \ --base-ref "$COVERAGE_BASE" \ "${module_args[@]}" - name: Enforce mature CPU-scope coverage and immutable quality policy run: | "$TEST_ENV/bin/python" scripts/check_quality_policy.py \ --policy ci/test-quality-policy.json \ --coverage "test-results/coverage-py${{ matrix.python-version }}.json" "$TEST_ENV/bin/python" scripts/check_conditional_policy.py "$TEST_ENV/bin/python" scripts/check_test_risk_map.py - name: Write normalized test trend evidence if: always() env: COVERAGE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} run: | base_args=() if [ -f test-results/base-coverage-py3.12.json ]; then base_args=(--base-coverage test-results/base-coverage-py3.12.json) fi "$TEST_ENV/bin/python" scripts/write_test_evidence.py \ --coverage "test-results/coverage-py${{ matrix.python-version }}.json" \ --junit "test-results/junit-py${{ matrix.python-version }}.xml" \ --head-sha "$GITHUB_SHA" \ --base-sha "$COVERAGE_BASE" \ --python-version "${{ matrix.python-version }}" \ --output "test-results/test-trend-py${{ matrix.python-version }}.json" \ "${base_args[@]}" - name: Enforce normalized test duration budgets run: | "$TEST_ENV/bin/python" scripts/check_quality_policy.py \ --policy ci/test-quality-policy.json \ --evidence "test-results/test-trend-py${{ matrix.python-version }}.json" - name: Upload test and coverage evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-evidence-py${{ matrix.python-version }} path: test-results/ if-no-files-found: error retention-days: 90 checkpoint-windows: name: Checkpoint contracts (Windows) runs-on: windows-latest timeout-minutes: 15 env: CUDA_VISIBLE_DEVICES: "" HF_DATASETS_OFFLINE: "1" HF_HUB_DISABLE_TELEMETRY: "1" HF_HUB_OFFLINE: "1" TOKENIZERS_PARALLELISM: "false" TRANSFORMERS_OFFLINE: "1" steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install locked test environment shell: pwsh run: | python -m pip install "uv==$env:UV_VERSION" $testEnv = Join-Path $env:RUNNER_TEMP "obliteratus-checkpoint-windows" $env:UV_PROJECT_ENVIRONMENT = $testEnv uv sync --locked --no-default-groups --extra dev --no-editable "CHECKPOINT_TEST_ENV=$testEnv" >> $env:GITHUB_ENV - name: Run Windows checkpoint contracts shell: pwsh run: | $python = Join-Path $env:CHECKPOINT_TEST_ENV "Scripts/python.exe" & $python -m pytest ` tests/test_checkpoint_atomicity.py ` tests/test_persistence_contracts.py ` tests/test_persistence_pipeline.py ` --no-cov ` --junitxml="$env:RUNNER_TEMP/checkpoint-windows.xml" ` -q - name: Upload Windows checkpoint evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: checkpoint-evidence-windows-py3.12 path: ${{ runner.temp }}/checkpoint-windows.xml if-no-files-found: error retention-days: 90 quality-depth: name: Quality depth runs-on: ubuntu-latest timeout-minutes: 45 env: CUDA_VISIBLE_DEVICES: "" HF_DATASETS_OFFLINE: "1" HF_HUB_DISABLE_TELEMETRY: "1" HF_HUB_OFFLINE: "1" TOKENIZERS_PARALLELISM: "false" TRANSFORMERS_OFFLINE: "1" QUALITY_ENV: /tmp/obliteratus-quality-env steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install locked quality tooling run: | python -m pip install "uv==${UV_VERSION}" UV_PROJECT_ENVIRONMENT="$QUALITY_ENV" \ uv sync --locked --extra dev --group quality --no-editable mkdir -p quality-evidence - name: Run deterministic order and hash-seed repeats run: >- "$QUALITY_ENV/bin/python" scripts/run_repeat_gate.py --output quality-evidence/repeat-gate.json - name: Run bounded selective mutation gate env: BLIS_NUM_THREADS: "1" MKL_NUM_THREADS: "1" NUMEXPR_NUM_THREADS: "1" OMP_THREAD_LIMIT: "1" OMP_NUM_THREADS: "1" OPENBLAS_NUM_THREADS: "1" VECLIB_MAXIMUM_THREADS: "1" run: | "$QUALITY_ENV/bin/python" scripts/check_mutation_targets.py prepare # shellcheck disable=SC2016 /usr/bin/time -f 'elapsed_seconds=%e\nmax_rss_kb=%M' \ -o quality-evidence/mutation-time.txt \ bash -euo pipefail -c ' "$QUALITY_ENV/bin/python" scripts/prepare_mutation_coverage.py prepare-coverage --max-children 8 "$QUALITY_ENV/bin/python" scripts/prepare_mutation_coverage.py prepare-stats --max-children 8 PATH="$QUALITY_ENV/bin:$PATH" \ "$QUALITY_ENV/bin/python" scripts/run_prepared_mutmut.py run --max-children 8 ' "$QUALITY_ENV/bin/python" scripts/check_mutation_targets.py check "$QUALITY_ENV/bin/mutmut" results > quality-evidence/mutation-survivors.txt "$QUALITY_ENV/bin/mutmut" export-cicd-stats cp mutants/mutmut-cicd-stats.json quality-evidence/mutation-stats.json cp mutants/mutmut-stats.json quality-evidence/mutation-test-selection.json "$QUALITY_ENV/bin/python" scripts/check_mutation_score.py \ quality-evidence/mutation-stats.json --minimum 85.0 - name: Write normalized quality trend evidence if: always() env: COVERAGE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} run: | evidence_args=() if [ -f quality-evidence/repeat-gate.json ]; then evidence_args+=(--repeat quality-evidence/repeat-gate.json) fi if [ -f quality-evidence/mutation-stats.json ]; then evidence_args+=(--mutation quality-evidence/mutation-stats.json) fi "$QUALITY_ENV/bin/python" scripts/write_test_evidence.py \ --head-sha "$GITHUB_SHA" \ --base-sha "$COVERAGE_BASE" \ --python-version "3.12" \ --output quality-evidence/quality-trend-py3.12.json \ "${evidence_args[@]}" - name: Enforce repeat duration budgets run: | "$QUALITY_ENV/bin/python" scripts/check_quality_policy.py \ --policy ci/test-quality-policy.json \ --evidence quality-evidence/quality-trend-py3.12.json - name: Upload quality-depth evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: quality-depth-py3.12 path: quality-evidence/ if-no-files-found: error retention-days: 90 supply-chain: name: Supply chain runs-on: ubuntu-latest timeout-minutes: 30 env: EVIDENCE: /tmp/supply-chain-evidence GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" GITLEAKS_VERSION: "8.30.1" RUNTIME_ENV: /tmp/obliteratus-runtime SUPPLY_TOOLS: /tmp/obliteratus-supply-tools steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" cache: pip cache-dependency-path: | pyproject.toml uv.lock - name: Install locked supply-chain tooling run: | python -m pip install "uv==${UV_VERSION}" uv lock --check UV_PROJECT_ENVIRONMENT="$RUNTIME_ENV" \ uv sync --locked --all-extras --no-default-groups --no-editable UV_PROJECT_ENVIRONMENT="$SUPPLY_TOOLS" \ uv sync --locked --only-group ci mkdir -p "$EVIDENCE" cd "$RUNNER_TEMP" "$RUNTIME_ENV/bin/python" -I - <<'PY' | tee "$EVIDENCE/spaces-import.txt" import gradio import app assert type(app.demo).__name__ == "Blocks" print(f"installed all-extras app import: {app.__file__}") print(f"gradio: {gradio.__version__}") PY - name: Validate exception policy run: >- python scripts/check_supply_chain_policy.py policy --policy ci/supply-chain-policy.json - name: Scan checkout for secrets with redacted evidence run: | archive="$RUNNER_TEMP/gitleaks.tar.gz" curl -fsSLo "$archive" \ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" echo "${GITLEAKS_SHA256} ${archive}" | sha256sum -c - tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks set +e "$RUNNER_TEMP/gitleaks" dir "$GITHUB_WORKSPACE" \ --redact=100 \ --report-format json \ --report-path "$EVIDENCE/gitleaks.json" \ --no-banner \ --no-color status=$? set -e echo "$status" > "$EVIDENCE/gitleaks.status" - name: Collect vulnerability evidence for supported Python versions run: | for version in 3.10 3.11 3.12; do set +e uv --preview-features audit-command,json-output audit \ --locked \ --python-version "$version" \ --python-platform linux \ --output-format json > "$EVIDENCE/uv-audit-py${version}.json" status=$? set -e echo "$status" > "$EVIDENCE/uv-audit-py${version}.status" done - name: Build wheel and bind CycloneDX SBOM run: | mkdir -p "$EVIDENCE/dist" "$SUPPLY_TOOLS/bin/python" -m build --wheel --outdir "$EVIDENCE/dist" uv --preview-features sbom-export export \ --locked \ --format cyclonedx1.5 \ --all-extras \ --no-dev \ --no-editable \ --output-file "$EVIDENCE/obliteratus.cdx.unbound.json" mapfile -t wheels < <(find "$EVIDENCE/dist" -maxdepth 1 -type f -name '*.whl' -print) if [ "${#wheels[@]}" -ne 1 ]; then echo "expected exactly one wheel, found ${#wheels[@]}" exit 1 fi python scripts/check_supply_chain_policy.py sbom \ --input "$EVIDENCE/obliteratus.cdx.unbound.json" \ --wheel "${wheels[0]}" \ --output "$EVIDENCE/obliteratus.cdx.json" sha256sum "${wheels[0]}" > "$EVIDENCE/distribution.SHA256SUM" - name: Collect packaged-dependency license inventory run: >- "$SUPPLY_TOOLS/bin/pip-licenses" --python "$RUNTIME_ENV/bin/python" --format json --output-file "$EVIDENCE/licenses.json" - name: Enforce vulnerability, secret, and license policies run: | for version in 3.10 3.11 3.12; do python scripts/check_supply_chain_policy.py audit \ --policy ci/supply-chain-policy.json \ --evidence "$EVIDENCE/uv-audit-py${version}.json" \ --scanner-status "$EVIDENCE/uv-audit-py${version}.status" \ --decision "$EVIDENCE/uv-audit-py${version}.decision.json" done python scripts/check_supply_chain_policy.py secrets \ --policy ci/supply-chain-policy.json \ --evidence "$EVIDENCE/gitleaks.json" \ --scanner-status "$EVIDENCE/gitleaks.status" \ --decision "$EVIDENCE/gitleaks.decision.json" python scripts/check_supply_chain_policy.py licenses \ --policy ci/supply-chain-policy.json \ --evidence "$EVIDENCE/licenses.json" \ --decision "$EVIDENCE/licenses.decision.json" - name: Upload supply-chain evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: supply-chain-evidence-py3.12 path: /tmp/supply-chain-evidence/ if-no-files-found: error retention-days: 14