diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67d365e..7ff63f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,46 @@ on: - main jobs: + changes: + name: Detect Rust changes + runs-on: ubuntu-latest + outputs: + rust: ${{ steps.filter.outputs.rust }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + # Avoids unnecessary Rust jobs running on `push` events; paths-filter + # needs the prior commit so it can do a proper diff, else it sees all + # files as new + fetch-depth: 2 + + # Doing a check here instead of using paths in our trigger conditions + # so that branch protection rules which require our Rust jobs aren't + # unsatisfied + - name: Filter + id: filter + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d #v4.0.3 + with: + filters: | + rust: + - "cli-engine/**" + - "cli-engine-macros/**" + - "Cargo.toml" + - "Cargo.lock" + - "rust-toolchain.toml" + - ".github/workflows/ci.yml" + rust: name: Rust + needs: changes + # `always()` overrides the implicit skip-on-upstream-failure default, so + # a `changes` failure (e.g. paths-filter erroring) can't silently skip + # the one check branch protection requires — it fails safe into running + # Rust CI rather than trusting an output that was never produced. + if: | + always() && + (needs.changes.result != 'success' || needs.changes.outputs.rust == 'true') runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/rust-toolchain-bump.yml b/.github/workflows/rust-toolchain-bump.yml new file mode 100644 index 0000000..20762b4 --- /dev/null +++ b/.github/workflows/rust-toolchain-bump.yml @@ -0,0 +1,66 @@ +name: Bump Rust Toolchain + +on: + schedule: + - cron: "13 7 * * 1" + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +env: + CARGO_TERM_COLOR: always + +jobs: + bump: + name: Check for a newer stable Rust + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + + # Explicit `toolchain: stable` bypasses rust-toolchain.toml so this + # always installs the current real latest, not our pinned version. + - name: Install latest stable Rust + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 + with: + toolchain: stable + + - name: Record latest stable version + id: latest + run: echo "version=$(rustc --version | cut -d' ' -f2)" >> "$GITHUB_OUTPUT" + + - name: Update pinned toolchain + run: | + sed \ + -i \ + -E "s/^channel = \".*\"/channel = \"${{ steps.latest.outputs.version }}\"/" \ + rust-toolchain.toml + + # No-op (and no PR) when rust-toolchain.toml already has this version, + # since there's nothing left for create-pull-request to commit. + # + # GitHub does not trigger `pull_request`/`push` workflow runs for + # branches/PRs created with the default GITHUB_TOKEN (anti-recursion + # protection), so the resulting PR would never run ci.yml unless a PAT + # or GitHub App token is supplied here instead. Falls back to + # GITHUB_TOKEN (today's no-auto-CI behavior) until that secret exists. + - name: Open pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 #v8.1.1 + with: + token: ${{ secrets.AUTOMATED_PRS_TOKEN || secrets.GITHUB_TOKEN }} + commit-message: "chore(rust): bump pinned toolchain to ${{ steps.latest.outputs.version }}" + title: "chore(rust): bump pinned toolchain to ${{ steps.latest.outputs.version }}" + body: | + Weekly check found a newer stable Rust release. + + `rust-toolchain.toml` is now pinned to `${{ steps.latest.outputs.version }}`. The + normal CI checks run on this PR like any other — if the new toolchain introduces + clippy/rustdoc/test failures, push fixes to this branch before merging so the bump + and its fixes land together. + branch: chore/bump-rust-toolchain + delete-branch: true + labels: | + rust + dependencies diff --git a/cli-engine/src/auth/mod.rs b/cli-engine/src/auth/mod.rs index befb684..45fd6d6 100644 --- a/cli-engine/src/auth/mod.rs +++ b/cli-engine/src/auth/mod.rs @@ -85,11 +85,14 @@ impl<'req> CredentialRequest<'req> { } } -#[async_trait] /// Named auth provider used by middleware and transport injectors. /// /// Implementations own their credential cache strategy. The framework only /// routes calls and passes command context (`env`, colon command path, and tier). +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Result`. +#[allow(clippy::double_must_use)] +#[async_trait] pub trait AuthProvider: Send + Sync + std::fmt::Debug { /// Stable provider registration name, for example `primary` or `oauth`. fn name(&self) -> &str; diff --git a/cli-engine/src/auth/storage.rs b/cli-engine/src/auth/storage.rs index f306133..3e84abd 100644 --- a/cli-engine/src/auth/storage.rs +++ b/cli-engine/src/auth/storage.rs @@ -59,6 +59,9 @@ impl<'key> CredentialKey<'key> { /// Values are opaque strings (typically JSON); the backend never interprets /// them, so it stays independent of any provider's token shape. Callers own /// (de)serialization and any validity/expiry checks. +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Option`/`Result`. +#[allow(clippy::double_must_use)] #[async_trait] pub trait CredentialStorage: Send + Sync + std::fmt::Debug { /// Loads the stored blob for `key`, or `None` when absent or unreadable. diff --git a/cli-engine/src/config.rs b/cli-engine/src/config.rs index 4f6f59f..0ba8466 100644 --- a/cli-engine/src/config.rs +++ b/cli-engine/src/config.rs @@ -32,7 +32,7 @@ //! [`crate::flags::resolve_default_output_format`]. //! //! where `${PREFIX}` is the app id sanitized by -//! [`app_id_env_prefix`](crate::flags::app_id_env_prefix). +//! [`crate::flags::app_id_env_prefix`]. use std::cell::Cell; use std::path::{Path, PathBuf}; diff --git a/cli-engine/src/middleware/mod.rs b/cli-engine/src/middleware/mod.rs index fad48ab..154dd74 100644 --- a/cli-engine/src/middleware/mod.rs +++ b/cli-engine/src/middleware/mod.rs @@ -380,7 +380,6 @@ fn identity_key(credential: &Credential) -> &str { } } -#[async_trait] /// Authorization hook called before business logic. /// /// The authorizer receives a [`CredentialResolver`] rather than an @@ -388,6 +387,10 @@ fn identity_key(credential: &Credential) -> &str { /// does not need identity never triggers a credential/auth flow. Call /// [`CredentialResolver::try_resolve`] only when a decision actually depends on /// the credential. +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Result`. +#[allow(clippy::double_must_use)] +#[async_trait] pub trait Authorizer: Send + Sync + std::fmt::Debug { /// Verifies whether `command_path` may run with the provided args, reason, and tier. async fn authorize( @@ -400,8 +403,11 @@ pub trait Authorizer: Send + Sync + std::fmt::Debug { ) -> Result<()>; } -#[async_trait] /// Audit hook called for success, error, denied, auth-error, and dry-run outcomes. +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Result`. +#[allow(clippy::double_must_use)] +#[async_trait] pub trait Auditor: Send + Sync + std::fmt::Debug { /// Appends an audit record. async fn append( @@ -414,8 +420,11 @@ pub trait Auditor: Send + Sync + std::fmt::Debug { ) -> Result<()>; } -#[async_trait] /// Activity hook for structured command lifecycle events. +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Result`. +#[allow(clippy::double_must_use)] +#[async_trait] pub trait ActivityEmitter: Send + Sync + std::fmt::Debug { /// Emits one completed command event. async fn emit(&self, event: ActivityEvent) -> Result<()>; diff --git a/cli-engine/src/transport/injector.rs b/cli-engine/src/transport/injector.rs index 46be9a7..db4a83a 100644 --- a/cli-engine/src/transport/injector.rs +++ b/cli-engine/src/transport/injector.rs @@ -14,8 +14,11 @@ use crate::{AuthProvider, CliCoreError, Result}; pub type TokenFunc = Arc Pin> + Send>> + Send + Sync>; -#[async_trait::async_trait] /// Mutates an outbound request with authentication material. +// async_trait's expansion attaches a bare `#[must_use]` to each generated +// method, duplicating the one already implied by `Result`. +#[allow(clippy::double_must_use)] +#[async_trait::async_trait] pub trait AuthInjector: Send + Sync + std::fmt::Debug { /// Adds auth headers or cookies to `request`. async fn inject(&self, request: &mut reqwest::Request) -> Result<()>; diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..0fd1b3a --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.99.0" +components = ["clippy", "rustfmt"]