From 1006f41e451ebe44718b1e930bcfe3e2ed1c72d6 Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Thu, 28 May 2026 16:44:58 +0200 Subject: [PATCH] Add GitHub CI security and release workflows --- .github/dependabot.yml | 17 +++++ .github/workflows/ci.yml | 90 ++++++++++++++++++++++ .github/workflows/codeql.yml | 44 +++++++++++ .github/workflows/dependency-review.yml | 21 ++++++ .github/workflows/release.yml | 99 +++++++++++++++++++++++++ .github/workflows/security.yml | 39 ++++++++++ README.md | 32 ++++++++ crates/geth-node/src/lib.rs | 12 +++ crates/geth/tests/bootstrap.rs | 28 +++++++ docs/roadmap.md | 14 ++++ 10 files changed, 396 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/security.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2e29cf5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + day: monday + time: "05:00" + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "05:30" + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..86693df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + fmt-clippy-docs: + name: fmt, clippy, docs + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Build docs + run: cargo doc --workspace --no-deps + + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + env: + GETH_TEST_SKIP_IROH: 1 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Check workspace + run: cargo check --workspace --all-targets + + - name: Test workspace without live Iroh integration + run: cargo test --workspace + + iroh-integration: + name: iroh integration smoke tests + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Run Iroh-heavy CLI integration tests serially + run: cargo test -p geth --test bootstrap -- --test-threads=1 + + - name: Run Iroh-heavy node integration tests + run: | + cargo test -p geth-node peer_ping_uses_signed_peer_card_over_iroh -- --nocapture + cargo test -p geth-node overlay_packets_route_over_dedicated_iroh_alpn_with_authorization -- --nocapture diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..f099082 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "43 3 * * 2" + workflow_dispatch: + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: read + contents: read + security-events: write + +env: + CARGO_TERM_COLOR: always + +jobs: + analyze: + name: Analyze Rust + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: rust + build-mode: manual + + - name: Build workspace for CodeQL + run: cargo build --workspace --all-targets + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..2359d50 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,21 @@ +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Review dependency changes + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d78d52d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,99 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + prerelease: + description: "Mark the GitHub release as a prerelease" + required: false + default: "true" + type: choice + options: ["true", "false"] + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: linux-x86_64 + binary: geth + - os: macos-latest + target: macos + binary: geth + - os: windows-latest + target: windows-x86_64 + binary: geth.exe + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Build release binary + run: cargo build --release -p geth + + - name: Package Unix artifact + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + version="${GITHUB_REF_NAME:-manual}" + pkg="geth-${version}-${{ matrix.target }}" + mkdir -p "${pkg}" dist + cp "target/release/${{ matrix.binary }}" "${pkg}/" + cp README.md "${pkg}/" + tar -czf "dist/${pkg}.tar.gz" "${pkg}" + + - name: Package Windows artifact + if: runner.os == 'Windows' + shell: pwsh + run: | + $version = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" } + $pkg = "geth-$version-${{ matrix.target }}" + New-Item -ItemType Directory -Force -Path $pkg, dist | Out-Null + Copy-Item "target/release/${{ matrix.binary }}" "$pkg/" + Copy-Item README.md "$pkg/" + Compress-Archive -Path "$pkg/*" -DestinationPath "dist/$pkg.zip" -Force + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: geth-${{ matrix.target }} + path: dist/* + if-no-files-found: error + + github-release: + name: publish GitHub release + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + prerelease: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.prerelease == 'true' }} + generate_release_notes: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..ba9fb2f --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,39 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + cargo-audit: + name: RustSec cargo-audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-audit + uses: taiki-e/install-action@cargo-audit + + - name: Scan Cargo.lock for advisories + run: cargo audit --deny warnings diff --git a/README.md b/README.md index ed42cd0..32035ac 100644 --- a/README.md +++ b/README.md @@ -620,6 +620,38 @@ If a command fails, the daemon error includes a `next:` line for common recovery paths such as importing a peer card, running `auth explain`, granting a missing capability, or creating/registering a missing resource. +## CI, Security, And Releases + +GitHub Actions workflows live under `.github/workflows/`: + +- `ci.yml` runs formatting, clippy, docs, `cargo check`, and workspace tests on + Linux, macOS, and Windows. Cross-platform test jobs set + `GETH_TEST_SKIP_IROH=1` so deterministic unit and integration coverage can be + required while Iroh-heavy daemon-to-daemon tests continue to mature. +- `ci.yml` also has a visible Ubuntu Iroh integration smoke job for the full + network-heavy paths. It is marked `continue-on-error` until the local Iroh + tests are reliable enough to make required. +- `security.yml` runs RustSec `cargo audit` on pushes, pull requests, manual + dispatch, and a weekly schedule. +- `codeql.yml` builds the Rust workspace for GitHub CodeQL analysis. +- `dependency-review.yml` blocks pull requests that introduce vulnerable + dependency changes at moderate severity or higher. +- `release.yml` builds release archives for Linux, macOS, and Windows, uploads + them as artifacts, and publishes them on `v*` tags or manual dispatch. +- `.github/dependabot.yml` opens weekly Cargo and GitHub Actions update PRs. + +Local equivalents remain: + +```sh +cargo fmt --all -- --check +cargo check --workspace --all-targets +cargo clippy --workspace --all-targets -- -D warnings +GETH_TEST_SKIP_IROH=1 cargo test --workspace +``` + +Run the Iroh-heavy tests without `GETH_TEST_SKIP_IROH` when working on endpoint, +peer-card, sync, overlay, or remote module behavior. + ## Authorization Direction The MVP defines the split between: diff --git a/crates/geth-node/src/lib.rs b/crates/geth-node/src/lib.rs index 68d1acd..07955d9 100644 --- a/crates/geth-node/src/lib.rs +++ b/crates/geth-node/src/lib.rs @@ -12453,6 +12453,10 @@ mod tests { use super::*; use tokio::io::AsyncReadExt; + fn skip_iroh_integration_tests() -> bool { + std::env::var_os("GETH_TEST_SKIP_IROH").is_some() + } + #[derive(Debug, PartialEq, Eq)] enum RemoteGuardKind { Capability, @@ -13121,6 +13125,10 @@ mod tests { #[tokio::test] async fn peer_ping_uses_signed_peer_card_over_iroh() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left home"); let right_home = tempfile::tempdir().expect("right home"); let left_paths = GethPaths::from_home(left_home.path()); @@ -14719,6 +14727,10 @@ mod tests { #[tokio::test] async fn overlay_packets_route_over_dedicated_iroh_alpn_with_authorization() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left home"); let right_home = tempfile::tempdir().expect("right home"); let left_paths = GethPaths::from_home(left_home.path()); diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs index 1518468..5327409 100644 --- a/crates/geth/tests/bootstrap.rs +++ b/crates/geth/tests/bootstrap.rs @@ -1,6 +1,10 @@ use std::process::{Child, Command}; use std::time::{Duration, Instant}; +fn skip_iroh_integration_tests() -> bool { + std::env::var_os("GETH_TEST_SKIP_IROH").is_some() +} + fn unix_sockets_available(home: &std::path::Path) -> bool { let probe = home.join("probe.sock"); match std::os::unix::net::UnixListener::bind(&probe) { @@ -248,6 +252,10 @@ fn geth_status_against_running_daemon() { #[test] fn peer_ping_uses_daemon_owned_iroh_endpoint() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left tempdir"); let right_home = tempfile::tempdir().expect("right tempdir"); if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) { @@ -329,6 +337,10 @@ fn peer_ping_uses_daemon_owned_iroh_endpoint() { #[test] fn sync_now_completes_owner_approved_node_enrollment_flow() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } if !ssh_keygen_available() { return; } @@ -585,6 +597,10 @@ fn sync_now_completes_owner_approved_node_enrollment_flow() { #[test] fn denied_remote_operations_do_not_mutate_serving_node_state() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left tempdir"); let right_home = tempfile::tempdir().expect("right tempdir"); if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) { @@ -744,6 +760,10 @@ fn denied_remote_operations_do_not_mutate_serving_node_state() { #[test] fn unsigned_keychain_and_auth_ops_are_rejected_during_peer_sync() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left tempdir"); let right_home = tempfile::tempdir().expect("right tempdir"); if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) { @@ -903,6 +923,10 @@ fn unsigned_keychain_and_auth_ops_are_rejected_during_peer_sync() { #[test] fn invalidly_signed_keychain_and_auth_ops_are_rejected_during_peer_sync() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } let left_home = tempfile::tempdir().expect("left tempdir"); let right_home = tempfile::tempdir().expect("right tempdir"); if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) { @@ -1107,6 +1131,10 @@ fn invalidly_signed_keychain_and_auth_ops_are_rejected_during_peer_sync() { #[test] fn conflicting_keychain_and_auth_ops_are_rejected_during_peer_sync() { + if skip_iroh_integration_tests() { + eprintln!("skipping Iroh integration test because GETH_TEST_SKIP_IROH is set"); + return; + } if !ssh_keygen_available() { return; } diff --git a/docs/roadmap.md b/docs/roadmap.md index 97e38c7..2161531 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -265,6 +265,20 @@ control, local CAS, service installation, and written architecture decisions. - Windows install targets a per-user scheduled task. - Tests verify generated definitions do not target privileged system services. +- `[x]` GitHub CI, security, and release automation. + Acceptance criteria: + - `[x]` CI runs formatting, clippy, docs, check, and deterministic workspace + tests on Linux, macOS, and Windows. + - `[x]` Iroh-heavy daemon-to-daemon tests are represented by a visible + network integration job instead of blocking deterministic platform coverage. + - `[x]` RustSec advisory scanning runs on pull requests, pushes, manual + dispatch, and a weekly schedule. + - `[x]` CodeQL and dependency review workflows are present for GitHub-native + security scanning. + - `[x]` Release workflow builds Linux, macOS, and Windows archives for `v*` + tags and manual dispatch. + - `[x]` Dependabot is configured for Cargo and GitHub Actions updates. + - `[x]` Bootstrap docs and ADRs. Acceptance criteria: - README explains what geth is and what it is not.