334 lines
13 KiB
YAML
334 lines
13 KiB
YAML
name: PR checks
|
|
|
|
on:
|
|
pull_request:
|
|
push:
|
|
branches: [main]
|
|
|
|
# Pull requests execute contributor-controlled build scripts, proc macros,
|
|
# tests, and binaries. They must never receive a write token or repository
|
|
# secrets. Publishing remains isolated in the tag-only release workflows.
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: pr-checks-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
CARGO_BUILD_JOBS: "2"
|
|
CARGO_INCREMENTAL: "0"
|
|
CARGO_TERM_COLOR: always
|
|
OPENSSL_NO_VENDOR: "1"
|
|
|
|
jobs:
|
|
policy:
|
|
name: PR / Policy
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 20
|
|
outputs:
|
|
base-sha: ${{ steps.base.outputs.sha }}
|
|
runtime: ${{ steps.changes.outputs.runtime }}
|
|
audit: ${{ steps.changes.outputs.audit }}
|
|
steps:
|
|
- name: Checkout merge result
|
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Resolve trusted base revision
|
|
id: base
|
|
shell: bash
|
|
env:
|
|
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
base_sha="$EVENT_BASE_SHA"
|
|
if [[ ! "$base_sha" =~ ^[0-9a-f]{40}$ ]] || [[ "$base_sha" == "0000000000000000000000000000000000000000" ]]; then
|
|
echo "GitHub did not provide a valid base revision" >&2
|
|
exit 1
|
|
fi
|
|
git cat-file -e "${base_sha}^{commit}"
|
|
|
|
policy_root="$RUNNER_TEMP/obscura-policy"
|
|
mkdir -p "$policy_root"
|
|
git show "$base_sha:scripts/ci/pr_policy.py" > "$policy_root/pr_policy.py"
|
|
git show "$base_sha:deny.toml" > "$policy_root/deny.toml"
|
|
|
|
# Container actions can only read paths mounted below the workspace.
|
|
# Replace any PR-supplied file before exposing the trusted config.
|
|
trusted_deny="$GITHUB_WORKSPACE/.github/.trusted-deny.toml"
|
|
rm -f -- "$trusted_deny"
|
|
install -m 0644 "$policy_root/deny.toml" "$trusted_deny"
|
|
printf 'sha=%s\n' "$base_sha" >> "$GITHUB_OUTPUT"
|
|
printf 'policy=%s\n' "$policy_root/pr_policy.py" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Classify change scope
|
|
id: changes
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ steps.base.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
runtime=false
|
|
audit=false
|
|
|
|
while IFS= read -r -d '' path; do
|
|
case "$path" in
|
|
Cargo.toml|Cargo.lock|deny.toml|*/Cargo.toml|*/Cargo.lock|.cargo/config|.cargo/config.toml)
|
|
runtime=true
|
|
audit=true
|
|
;;
|
|
.github/workflows/ci.yml)
|
|
runtime=true
|
|
audit=true
|
|
;;
|
|
scripts/ci/*)
|
|
;;
|
|
*.md|README*|LICENSE*|docs/*|.github/ISSUE_TEMPLATE/*|.github/PULL_REQUEST_TEMPLATE*|.github/CODEOWNERS|.gitignore|.gitattributes|.editorconfig)
|
|
;;
|
|
*)
|
|
runtime=true
|
|
;;
|
|
esac
|
|
# Disable rename detection so both sides of a rename are classified.
|
|
# Otherwise moving runtime code to a documentation-looking path could
|
|
# incorrectly skip the runtime checks.
|
|
done < <(git diff --no-ext-diff --no-renames --name-only -z "$BASE_SHA" "$GITHUB_SHA")
|
|
|
|
printf 'runtime=%s\n' "$runtime" >> "$GITHUB_OUTPUT"
|
|
printf 'audit=%s\n' "$audit" >> "$GITHUB_OUTPUT"
|
|
printf 'Change scope: runtime=%s audit=%s\n' "$runtime" "$audit"
|
|
|
|
- name: Check PR policy
|
|
env:
|
|
BASE_SHA: ${{ steps.base.outputs.sha }}
|
|
run: python3 "${{ steps.base.outputs.policy }}" --base "$BASE_SHA" --head "$GITHUB_SHA"
|
|
|
|
- name: Check dependency advisories, licenses, bans, and sources
|
|
if: steps.changes.outputs.audit == 'true'
|
|
uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
|
|
with:
|
|
arguments: --all-features --config .github/.trusted-deny.toml
|
|
command: check advisories bans licenses sources
|
|
|
|
core:
|
|
name: PR / Core
|
|
needs: policy
|
|
# A successful main build warms dependencies for read-only PR jobs.
|
|
if: >-
|
|
!cancelled() &&
|
|
((github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
|
(needs.policy.result == 'success' && needs.policy.outputs.runtime == 'true'))
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 90
|
|
steps:
|
|
- name: Checkout merge result
|
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
clang cmake libclang-dev libssl-dev llvm-dev pkg-config
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
|
|
|
- name: Test obstacle comparison gate
|
|
run: python3 -m unittest discover -s scripts/ci -p 'test_*.py' -v
|
|
|
|
- name: Install cargo-nextest
|
|
uses: taiki-e/install-action@b20dedce73af6905cdc30d6611090c9b67557c8d # v2
|
|
with:
|
|
tool: nextest
|
|
fallback: none
|
|
|
|
- name: Restore Rust dependency cache
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
|
with:
|
|
shared-key: obscura-ci-dependencies
|
|
# Only successful main builds save. PRs remain restore-only and
|
|
# release workflows do not consume this cache.
|
|
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
|
|
|
- name: Build render release
|
|
run: cargo build --release -p obscura-cli --bins --features render
|
|
|
|
- name: Test render configuration
|
|
run: cargo nextest run --release --features render --no-fail-fast
|
|
|
|
- name: Stage candidate binary for comparison
|
|
run: install -D -m 0755 target/release/obscura "$RUNNER_TEMP/obscura-ci/obscura-pr"
|
|
|
|
- name: Checkout pinned obstacle course
|
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
repository: h4ckf0r0day/obscura-benchmark
|
|
ref: 2d08d9a8e860e423b47209434a9fba9c75e6eee6
|
|
path: obscura-benchmark
|
|
persist-credentials: false
|
|
|
|
- name: Require all 33 obstacle stages
|
|
run: |
|
|
OBSCURA_BIN="$RUNNER_TEMP/obscura-ci/obscura-pr" \
|
|
python3 obscura-benchmark/obstacle-course/run.py \
|
|
--json --runs 1 --warmup 0 > "$RUNNER_TEMP/obscura-ci/obstacle-candidate.json"
|
|
|
|
- name: Upload obstacle evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: core-obstacle-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/obscura-ci/obstacle-candidate.json
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
- name: Build render and stealth release
|
|
run: cargo build --release -p obscura-cli --bins --features render,stealth
|
|
|
|
- name: Test the stealth transport
|
|
run: |
|
|
# The gzip fixture binds loopback. Keep the ordinary suite under the
|
|
# default SSRF policy, then opt in only for that single focused test.
|
|
cargo nextest run --release -p obscura-net --features stealth \
|
|
--no-fail-fast -- --skip stealth_client_decodes_gzip_response
|
|
OBSCURA_ALLOW_PRIVATE_NETWORK=1 cargo nextest run --release \
|
|
-p obscura-net --features stealth \
|
|
stealth_client_decodes_gzip_response --no-fail-fast
|
|
|
|
- name: Build no-render release
|
|
run: cargo build --release -p obscura-cli --bins --no-default-features
|
|
|
|
- name: Test no-render configuration
|
|
run: |
|
|
# `obscura` uses its default `api` feature for the public library; an
|
|
# empty feature set is not a supported crate configuration. Test that
|
|
# no-render API normally and every non-render crate with defaults
|
|
# disabled. `obscura-render` has paint-only test fixtures, so verify
|
|
# its feature-minimal library separately without compiling those
|
|
# fixtures.
|
|
cargo nextest run --release -p obscura --no-fail-fast
|
|
cargo nextest run --release --workspace --exclude obscura \
|
|
--exclude obscura-render \
|
|
--no-default-features --no-fail-fast
|
|
cargo check --release -p obscura-render --no-default-features
|
|
|
|
- name: Build no-render and stealth release
|
|
run: cargo build --release -p obscura-cli --bins --no-default-features --features stealth
|
|
|
|
- name: Upload candidate binary
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: obscura-pr-${{ github.run_id }}
|
|
path: ${{ runner.temp }}/obscura-ci/obscura-pr
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
retention-days: 1
|
|
|
|
performance:
|
|
name: PR / Performance
|
|
if: needs.policy.outputs.runtime == 'true'
|
|
needs: [policy, core]
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 75
|
|
steps:
|
|
- name: Checkout trusted base revision
|
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
ref: ${{ needs.policy.outputs.base-sha }}
|
|
persist-credentials: false
|
|
|
|
- name: Checkout pinned obstacle course
|
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
with:
|
|
repository: h4ckf0r0day/obscura-benchmark
|
|
ref: 2d08d9a8e860e423b47209434a9fba9c75e6eee6
|
|
path: obscura-benchmark
|
|
persist-credentials: false
|
|
|
|
- name: Download candidate binary
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: obscura-pr-${{ github.run_id }}
|
|
path: ${{ runner.temp }}/obscura-ci
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
clang cmake libclang-dev libssl-dev llvm-dev pkg-config time
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
|
|
|
- name: Restore Rust dependency cache
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
|
with:
|
|
shared-key: obscura-ci-dependencies
|
|
save-if: false
|
|
|
|
- name: Build base revision
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ needs.policy.outputs.base-sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
script_root="$RUNNER_TEMP/obscura-ci-scripts"
|
|
mkdir -p "$script_root"
|
|
git show "$BASE_SHA:scripts/ci/perf_smoke.py" > "$script_root/perf_smoke.py"
|
|
git show "$BASE_SHA:scripts/ci/compare_obstacle.py" > "$script_root/compare_obstacle.py"
|
|
# Checkout and cache metadata both belong to the trusted base. Build
|
|
# in the restored target directory, not a separate empty worktree.
|
|
cargo build --release -p obscura-cli --bins --features render
|
|
install -D -m 0755 \
|
|
target/release/obscura \
|
|
"$RUNNER_TEMP/obscura-ci/obscura-base"
|
|
chmod 0755 "$RUNNER_TEMP/obscura-ci/obscura-pr"
|
|
|
|
- name: Run base and candidate obstacle courses
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
results_root="$RUNNER_TEMP/obscura-ci-results"
|
|
mkdir -p "$results_root"
|
|
set +e
|
|
OBSCURA_BIN="$RUNNER_TEMP/obscura-ci/obscura-base" \
|
|
python3 obscura-benchmark/obstacle-course/run.py \
|
|
--json --runs 3 --warmup 1 > "$results_root/obstacle-base.json"
|
|
base_status=$?
|
|
OBSCURA_BIN="$RUNNER_TEMP/obscura-ci/obscura-pr" \
|
|
python3 obscura-benchmark/obstacle-course/run.py \
|
|
--json --runs 3 --warmup 1 > "$results_root/obstacle-pr.json"
|
|
pr_status=$?
|
|
set -e
|
|
printf 'Obstacle commands exited base=%s candidate=%s\n' "$base_status" "$pr_status"
|
|
python3 -m json.tool "$results_root/obstacle-base.json" >/dev/null
|
|
python3 -m json.tool "$results_root/obstacle-pr.json" >/dev/null
|
|
|
|
- name: Run interleaved latency and RSS smoke benchmark
|
|
run: |
|
|
python3 "$RUNNER_TEMP/obscura-ci-scripts/perf_smoke.py" \
|
|
--base "$RUNNER_TEMP/obscura-ci/obscura-base" \
|
|
--candidate "$RUNNER_TEMP/obscura-ci/obscura-pr" \
|
|
--output "$RUNNER_TEMP/obscura-ci-results/perf-smoke.json"
|
|
|
|
- name: Compare obstacle correctness and latency
|
|
run: |
|
|
python3 "$RUNNER_TEMP/obscura-ci-scripts/compare_obstacle.py" \
|
|
--base "$RUNNER_TEMP/obscura-ci-results/obstacle-base.json" \
|
|
--candidate "$RUNNER_TEMP/obscura-ci-results/obstacle-pr.json"
|
|
|
|
- name: Upload comparison evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: pr-comparison-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/obscura-ci-results
|
|
if-no-files-found: warn
|
|
retention-days: 7
|