Repository navigation
fix(approval): recheck reusable grants and shrink judge caches #165
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| pull_request: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| # Lint levels live in `[workspace.lints]` in the root Cargo.toml so local and | |
| # CI runs agree; don't add a blanket RUSTFLAGS here. | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| rust: | |
| name: Rust | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # This job executes repository code (cargo build/test); don't persist | |
| # the token in git config. | |
| persist-credentials: false | |
| submodules: recursive | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy, llvm-tools-preview | |
| - uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-llvm-cov | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Test native coverage gate | |
| run: python .github/scripts/check-native-coverage_tests.py | |
| - name: Check formatting | |
| run: cargo fmt --all -- --check | |
| - name: Clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Build | |
| run: cargo build --all-targets --all-features | |
| - name: Provision Windows audit test namespace | |
| if: ${{ runner.os == 'Windows' }} | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| # The hosted D: runner-temp ancestors fail the engine's actual owner/ | |
| # mutation ACL checks. Provision below the current user's normal profile; | |
| # native tests still inspect and pin every ancestor, including C:\. | |
| $auditRoot = Join-Path $env:USERPROFILE 'tinysecurity-audit-ci' | |
| New-Item -ItemType Directory -Force $auditRoot | Out-Null | |
| $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() | |
| $security = [System.Security.AccessControl.DirectorySecurity]::new() | |
| $security.SetOwner($identity.User) | |
| $security.SetAccessRuleProtection($true, $false) | |
| $rule = [System.Security.AccessControl.FileSystemAccessRule]::new( | |
| $identity.User, | |
| [System.Security.AccessControl.FileSystemRights]::FullControl, | |
| [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit', | |
| [System.Security.AccessControl.PropagationFlags]::None, | |
| [System.Security.AccessControl.AccessControlType]::Allow | |
| ) | |
| [void]$security.AddAccessRule($rule) | |
| Set-Acl -LiteralPath $auditRoot -AclObject $security | |
| "TINYSECURITY_AUDIT_TEST_ROOT=$auditRoot" >> $env:GITHUB_ENV | |
| - name: Windows approval namespace preserves restricted native drive paths | |
| if: ${{ runner.os == 'Windows' }} | |
| shell: pwsh | |
| run: | | |
| cargo test --locked --package tinysecurity-module --example approval_native_contract windows_approval_namespace_preserves_drive_path_and_opens_restricted_sink -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { throw 'native approval namespace regression failed' } | |
| - name: Verify Windows module through the restricted native copy | |
| if: ${{ runner.os == 'Windows' }} | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $libraryName = 'tinysecurity_module' | |
| $module = "target/debug/$libraryName.dll" | |
| $verifyRoot = Join-Path $env:RUNNER_TEMP 'tinysecurity-module-ci-verify' | |
| New-Item -ItemType Directory -Force $verifyRoot | Out-Null | |
| $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() | |
| $security = [System.Security.AccessControl.DirectorySecurity]::new() | |
| $security.SetOwner($identity.User) | |
| $security.SetAccessRuleProtection($true, $false) | |
| $rights = [System.Security.AccessControl.FileSystemRights]::FullControl | |
| $inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit' | |
| $propagation = [System.Security.AccessControl.PropagationFlags]::None | |
| $access = [System.Security.AccessControl.AccessControlType]::Allow | |
| foreach ($sidValue in @( | |
| $identity.User.Value, | |
| 'S-1-5-18', | |
| 'S-1-5-32-544' | |
| )) { | |
| $sid = [System.Security.Principal.SecurityIdentifier]::new($sidValue) | |
| $rule = [System.Security.AccessControl.FileSystemAccessRule]::new( | |
| $sid, | |
| $rights, | |
| $inheritance, | |
| $propagation, | |
| $access | |
| ) | |
| [void]$security.AddAccessRule($rule) | |
| } | |
| Set-Acl -LiteralPath $verifyRoot -AclObject $security | |
| $verifiedModule = Join-Path $verifyRoot "$libraryName.dll" | |
| Copy-Item -LiteralPath $module -Destination $verifiedModule | |
| # Collect independent ownership checks before aggregating failure; | |
| # one failed check must not hide the exact installed namespace result. | |
| $diagnosticFailure = $false | |
| cargo test --locked --package tinysecurity-module --example audit_native_contract -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true } | |
| cargo test --locked -p tinysecurity-audit windows_token_default_owner_explains_ordinary_created_file_owner -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true } | |
| cargo test --locked -p tinysecurity-audit windows_new_engine_file_is_owned_by_current_user_before_any_bytes -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true } | |
| cargo test --locked -p tinysecurity-audit windows_existing_foreign_owner_is_denied_without_owner_repair -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true } | |
| cargo test --locked -p tinysecurity-audit windows_protected_preprovisioned_namespace_commits_exact_retry -- --nocapture | |
| if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true } | |
| if ($diagnosticFailure) { throw 'native audit ownership regressions failed' } | |
| cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native verifier failed in workspace' } | |
| cargo run --locked --package tinysecurity-module --example audit_native_contract -- $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native audit verifier failed in workspace' } | |
| $verifier = (Resolve-Path 'target/debug/examples/verify_module.exe').Path | |
| cargo run --locked --package tinysecurity-module --example approval_native_contract -- $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native approval verifier failed in workspace' } | |
| cargo run --locked --package tinysecurity-module --example judge_native_contract -- $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native judge verifier failed in workspace' } | |
| $judgeVerifier = (Resolve-Path 'target/debug/examples/judge_native_contract.exe').Path | |
| $approvalVerifier = (Resolve-Path 'target/debug/examples/approval_native_contract.exe').Path | |
| $auditVerifier = (Resolve-Path 'target/debug/examples/audit_native_contract.exe').Path | |
| Push-Location $env:RUNNER_TEMP | |
| try { | |
| & $verifier $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native verifier failed outside workspace' } | |
| & $judgeVerifier $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native judge verifier failed outside workspace' } | |
| & $approvalVerifier $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native approval verifier failed outside workspace' } | |
| & $auditVerifier $verifiedModule | |
| if ($LASTEXITCODE -ne 0) { throw 'native audit verifier failed outside workspace' } | |
| } finally { | |
| Pop-Location | |
| } | |
| - name: Windows protected audit namespace and rotation behavior | |
| if: ${{ runner.os == 'Windows' }} | |
| run: cargo test --locked -p tinysecurity-audit windows_ -- --nocapture | |
| - name: Test | |
| run: cargo test --all-features | |
| - name: Test default features | |
| run: cargo test | |
| # `cargo build --all-targets` only *compiles* an example. `AGENTS.md` | |
| # promises the native loader verifier works, and a compiled | |
| # example can still fail on its first line. | |
| - name: Run the bundled example | |
| if: ${{ runner.os != 'Windows' }} | |
| shell: bash | |
| run: | | |
| case "$RUNNER_OS" in | |
| Linux) module=target/debug/libtinysecurity_module.so ;; | |
| macOS) module=target/debug/libtinysecurity_module.dylib ;; | |
| esac | |
| cargo run -p tinysecurity-module --example verify_module -- "$module" | |
| cargo run -p tinysecurity-module --example audit_native_contract -- "$module" | |
| cargo run -p tinysecurity-module --example approval_native_contract -- "$module" | |
| cargo run -p tinysecurity-module --example judge_native_contract -- "$module" | |
| # Installed examples must also work outside the workspace root. | |
| module="$(pwd)/$module" | |
| (cd target && ./debug/examples/verify_module "$module") | |
| (cd target && ./debug/examples/audit_native_contract "$module") | |
| (cd target && ./debug/examples/approval_native_contract "$module") | |
| (cd target && ./debug/examples/judge_native_contract "$module") | |
| # `crates/tinysecurity-bus` exists so a host can name the payload types | |
| # without compiling the module. That promise is invisible in a diff, | |
| # because a forbidden dependency arrives transitively through a feature | |
| # someone enabled one crate away — so it is asserted rather than | |
| # documented. | |
| # | |
| # The FORWARD form is required. `cargo tree -i <crate> -p tinysecurity-bus` | |
| # discards the `-p` scope, prints the whole-workspace inverse tree, and | |
| # exits 0 looking clean even when this crate is the one at fault. | |
| - name: Assert the contract crate stays transport-free | |
| run: | | |
| set -euo pipefail | |
| cargo metadata --format-version 1 --no-deps | jq -e ' | |
| [.packages[] | select(.name == "tinysecurity-bus") | .dependencies[] | |
| | select(.kind == null) | .name] | sort == ["serde", "thiserror"]' | |
| forbidden="$(cargo tree -p tinysecurity-bus -e normal,build --prefix none \ | |
| | grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2' || true)" | |
| if [ -n "$forbidden" ]; then | |
| echo "tinysecurity-bus pulled in a dependency its manifest forbids:" >&2 | |
| echo "$forbidden" >&2 | |
| echo >&2 | |
| echo "The contract is what a host compiles against. It must stay free" >&2 | |
| echo "of transports, async runtimes, HTTP clients and native libraries." >&2 | |
| exit 1 | |
| fi | |
| # Preserve Linux's established per-file gate and add the same strict | |
| # native JSON/LCOV inventory and changed-line gate to every platform. | |
| - name: Require 90% line coverage in every source file | |
| if: ${{ runner.os == 'Linux' }} | |
| run: .github/scripts/check-file-coverage.sh 90 coverage.json | |
| - name: Collect native coverage | |
| env: | |
| PR_BASE: ${{ github.event.pull_request.base.sha }} | |
| BASE_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| base="$PR_BASE" | |
| if [ -z "$base" ]; then | |
| base="$(git merge-base HEAD "refs/remotes/origin/$BASE_BRANCH")" | |
| fi | |
| case "$RUNNER_OS" in | |
| Linux) platform=linux ;; | |
| macOS) platform=macos ;; | |
| Windows) platform=windows ;; | |
| *) echo 'unsupported coverage platform' >&2; exit 1 ;; | |
| esac | |
| # Python supplies a native drive path on Windows; Bash PWD is an | |
| # MSYS path and cannot match cargo-llvm-cov's native filenames. | |
| checkout_root="$(python -c 'import pathlib; print(pathlib.Path.cwd())')" | |
| export COVERAGE_BASE="$base" COVERAGE_PLATFORM="$platform" | |
| python - <<'PYTHON' | |
| import json, os, pathlib, subprocess | |
| metadata = { | |
| 'checkout_root': str(pathlib.Path.cwd()), | |
| 'head': subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip(), | |
| 'base': os.environ['COVERAGE_BASE'], | |
| 'rustc': subprocess.check_output(['rustc', '--version', '--verbose']).decode('utf-8'), | |
| 'runner_os': os.environ['RUNNER_OS'], | |
| 'platform': os.environ['COVERAGE_PLATFORM'], | |
| } | |
| pathlib.Path('coverage-native-metadata.json').write_text(json.dumps(metadata, indent=2), encoding='utf-8') | |
| PYTHON | |
| cargo llvm-cov --locked --workspace --all-targets --all-features --json --output-path coverage-native.json | |
| # Report exports the instrumented all-feature/all-target run. Selection | |
| # flags are invalid for cargo-llvm-cov's export-only report subcommand. | |
| cargo llvm-cov report --lcov --output-path coverage-native.info | |
| python .github/scripts/check-native-coverage.py --json coverage-native.json --lcov coverage-native.info --base "$base" --root "$checkout_root" --platform "$platform" | |
| - name: Upload native coverage reports | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-native-${{ runner.os }}-${{ github.sha }} | |
| path: | | |
| coverage-native.json | |
| coverage-native.info | |
| coverage-native-metadata.json | |
| if-no-files-found: warn | |
| - name: Upload coverage report | |
| if: ${{ always() && runner.os == 'Linux' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-json-${{ matrix.os }} | |
| path: coverage.json | |
| if-no-files-found: ignore | |
| docs: | |
| name: Docs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build documentation | |
| env: | |
| RUSTDOCFLAGS: -D warnings | |
| run: cargo doc --no-deps --all-features | |
| msrv: | |
| name: Minimum supported Rust version | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| # `rust-version` is inherited from `[workspace.package]`, so every member | |
| # reports the same value. Read it off the package the module ships as | |
| # rather than off `packages[0]`, whose order cargo does not promise. | |
| - name: Read rust-version from Cargo.toml | |
| id: msrv | |
| run: | | |
| set -euo pipefail | |
| msrv="$(cargo metadata --format-version 1 --no-deps \ | |
| | jq -r '.packages[] | select(.name == "tinysecurity-module") | .rust_version')" | |
| if [[ -z "$msrv" || "$msrv" == "null" ]]; then | |
| echo "workspace.package.rust-version is not set in Cargo.toml" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$msrv" >> "$GITHUB_OUTPUT" | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: ${{ steps.msrv.outputs.version }} | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build with the declared MSRV | |
| run: cargo build --all-targets --all-features | |
| supply-chain: | |
| name: Supply chain | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Check advisories, licenses, bans, and sources | |
| uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| command: check all |