From 9c19fbb009447245cad4391f982479c34a770991 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 2 Oct 2026 13:33:23 -0700 Subject: [PATCH 1/3] fix(ci): unblock Rust CI on newer clippy/rustdoc lints and pin the toolchain Newer stable rustc/clippy started flagging async_trait's generated #[must_use] as redundant (clippy::double_must_use) and one doc link as having a redundant explicit target, both newly-stricter lints that broke an unrelated PR. Also pins rustc/cargo via rust-toolchain.toml (mirrored automatically by rustup locally and by setup-rust-toolchain in CI/release), adds a weekly workflow that opens a PR bumping that pin to the latest stable, and skips the Rust CI job on PRs/pushes that don't touch Rust code (via a job-level `if`, not a trigger-level path filter, so the required "Rust" status check still reports instead of hanging pending). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 32 ++++++++++++ .github/workflows/rust-toolchain-bump.yml | 59 +++++++++++++++++++++++ cli-engine/src/auth/mod.rs | 5 +- cli-engine/src/auth/storage.rs | 3 ++ cli-engine/src/config.rs | 2 +- cli-engine/src/middleware/mod.rs | 15 ++++-- cli-engine/src/transport/injector.rs | 5 +- rust-toolchain.toml | 3 ++ 8 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/rust-toolchain-bump.yml create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67d365e..e653776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,40 @@ 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 + # unsatisified + - 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 + if: 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..054c755 --- /dev/null +++ b/.github/workflows/rust-toolchain-bump.yml @@ -0,0 +1,59 @@ +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. + - name: Open pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 #v8.1.1 + with: + 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"] From 4a900a8fea7aced1418cd9740394e74d3abd88f3 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 2 Oct 2026 13:40:30 -0700 Subject: [PATCH 2/3] fix(ci): fail safe on change-detection failure, fix typo, flag token gap Copilot review on #122 caught two real issues: the Rust job's `if` skipped (not failed) when the `changes` job itself fails, which would've let a transient paths-filter error bypass the only required Rust check; and the bump workflow's default GITHUB_TOKEN means its generated PR won't trigger ci.yml at all (GitHub's anti-recursion protection), defeating the point of opening it as a normal PR. The first is fixed outright. The second needs a PAT/GitHub App secret only a repo admin can provision, so the token input now prefers one if present (`RUST_TOOLCHAIN_BUMP_TOKEN`) and falls back to today's behavior otherwise. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 10 ++++++++-- .github/workflows/rust-toolchain-bump.yml | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e653776..7ff63f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: # Doing a check here instead of using paths in our trigger conditions # so that branch protection rules which require our Rust jobs aren't - # unsatisified + # unsatisfied - name: Filter id: filter uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d #v4.0.3 @@ -40,7 +40,13 @@ jobs: rust: name: Rust needs: changes - if: needs.changes.outputs.rust == 'true' + # `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 index 054c755..c740f90 100644 --- a/.github/workflows/rust-toolchain-bump.yml +++ b/.github/workflows/rust-toolchain-bump.yml @@ -40,9 +40,16 @@ jobs: # 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.RUST_TOOLCHAIN_BUMP_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: | From 464ced69e679cf1f290ca4bf06d7a11edab401fd Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 2 Oct 2026 14:29:29 -0700 Subject: [PATCH 3/3] Switch to actual secret name --- .github/workflows/rust-toolchain-bump.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust-toolchain-bump.yml b/.github/workflows/rust-toolchain-bump.yml index c740f90..20762b4 100644 --- a/.github/workflows/rust-toolchain-bump.yml +++ b/.github/workflows/rust-toolchain-bump.yml @@ -49,7 +49,7 @@ jobs: - name: Open pull request uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 #v8.1.1 with: - token: ${{ secrets.RUST_TOOLCHAIN_BUMP_TOKEN || secrets.GITHUB_TOKEN }} + 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: |