From c1b0fb4daf5a08c60f3f7e5293e845f31baf68ee Mon Sep 17 00:00:00 2001 From: overtrue Date: Tue, 3 Nov 2026 08:29:31 +0800 Subject: [PATCH 1/3] feat(phase-2): prepare v0.1.37 release Signed-off-by: overtrue --- .github/workflows/ci.yml | 34 +++++++++------------------------- .github/workflows/package.yml | 26 +++++++++++++++++++++----- .github/workflows/release.yml | 6 +++++- Cargo.lock | 6 +++--- Cargo.toml | 8 ++++---- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8e929..b323a2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,23 +82,14 @@ jobs: name: Test (${{ matrix.name }}) runs-on: ${{ matrix.runner }} timeout-minutes: 60 - # manual_only lanes are GitHub-hosted and must not run on push or pull_request. - # Compared as a string: a boolean false in a matrix is easy to misread as the - # truthy string "false" once it crosses an expression boundary. - if: ${{ matrix.manual_only != 'yes' || github.event_name == 'workflow_dispatch' }} + # Select manual lanes before expansion; job-level conditions cannot use matrix. strategy: fail-fast: false matrix: - include: - - name: linux - runner: sm-standard-4 - manual_only: "no" - - name: macos-latest, manual - runner: macos-latest - manual_only: "yes" - - name: windows-latest, manual - runner: windows-latest - manual_only: "yes" + include: >- + ${{ fromJSON(github.event_name == 'workflow_dispatch' && + '[{"name":"linux","runner":"sm-standard-4","manual_only":"no"},{"name":"macos-latest, manual","runner":"macos-latest","manual_only":"yes"},{"name":"windows-latest, manual","runner":"windows-latest","manual_only":"yes"}]' || + '[{"name":"linux","runner":"sm-standard-4","manual_only":"no"}]') }} steps: - name: Flag billed GitHub-hosted runner if: matrix.manual_only == 'yes' @@ -121,20 +112,13 @@ jobs: name: Build (${{ matrix.name }}) runs-on: ${{ matrix.runner }} timeout-minutes: 90 - if: ${{ matrix.manual_only != 'yes' || github.event_name == 'workflow_dispatch' }} strategy: fail-fast: false matrix: - include: - - name: linux - runner: sm-standard-4 - manual_only: "no" - - name: macos-latest, manual - runner: macos-latest - manual_only: "yes" - - name: windows-latest, manual - runner: windows-latest - manual_only: "yes" + include: >- + ${{ fromJSON(github.event_name == 'workflow_dispatch' && + '[{"name":"linux","runner":"sm-standard-4","manual_only":"no"},{"name":"macos-latest, manual","runner":"macos-latest","manual_only":"yes"},{"name":"windows-latest, manual","runner":"windows-latest","manual_only":"yes"}]' || + '[{"name":"linux","runner":"sm-standard-4","manual_only":"no"}]') }} steps: - name: Flag billed GitHub-hosted runner if: matrix.manual_only == 'yes' diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index c279cd8..0ea9b51 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -58,6 +58,10 @@ jobs: tag: ${{ steps.resolve.outputs.tag }} head_sha: ${{ steps.resolve.outputs.head_sha }} steps: + - name: Checkout workflow source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false - name: Install GitHub CLI uses: ./.github/actions/setup-self-hosted with: @@ -195,17 +199,18 @@ jobs: rpm_arch: aarch64 artifact_name: rustfs-cli-linux-arm64 steps: - - name: Install file(1) - uses: ./.github/actions/setup-self-hosted - with: - # Used to prove the packaged binary is statically linked. - profile: file - name: Checkout exact build source uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: ref: ${{ needs.resolve.outputs.head_sha }} persist-credentials: false + - name: Install file(1) + uses: ./.github/actions/setup-self-hosted + with: + # Used to prove the packaged binary is statically linked. + profile: file + - name: Download binary artifact from release build uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: @@ -384,6 +389,11 @@ jobs: contents: write actions: read steps: + - name: Checkout exact build source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.resolve.outputs.head_sha }} + persist-credentials: false - name: Install GitHub CLI uses: ./.github/actions/setup-self-hosted with: @@ -467,8 +477,14 @@ jobs: runs-on: sm-standard-2 timeout-minutes: 15 permissions: + contents: read actions: read steps: + - name: Checkout exact build source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.resolve.outputs.head_sha }} + persist-credentials: false - name: Install AWS CLI uses: ./.github/actions/setup-self-hosted with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f87497c..143c0c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -419,6 +419,11 @@ jobs: permissions: contents: write steps: + - name: Checkout release source + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.build-check.outputs.source_ref }} + persist-credentials: false - name: Install AWS CLI uses: ./.github/actions/setup-self-hosted with: @@ -528,7 +533,6 @@ jobs: package-deb-rpm: name: Package DEB/RPM needs: [build-check, upload-release-assets] - timeout-minutes: 90 if: needs.build-check.outputs.is_release == 'true' uses: ./.github/workflows/package.yml with: diff --git a/Cargo.lock b/Cargo.lock index 0425192..38a67b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2763,7 +2763,7 @@ dependencies = [ [[package]] name = "rc-core" -version = "0.1.24" +version = "0.1.37" dependencies = [ "aes-gcm", "anyhow", @@ -2798,7 +2798,7 @@ dependencies = [ [[package]] name = "rc-s3" -version = "0.1.24" +version = "0.1.37" dependencies = [ "anyhow", "async-trait", @@ -3030,7 +3030,7 @@ dependencies = [ [[package]] name = "rustfs-cli" -version = "0.1.24" +version = "0.1.37" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index e7dc342..206a597 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "3" members = ["crates/cli", "crates/core", "crates/s3"] [workspace.package] -version = "0.1.24" +version = "0.1.37" edition = "2024" rust-version = "1.92" license = "MIT OR Apache-2.0" @@ -16,9 +16,9 @@ categories = ["command-line-utilities", "filesystem"] [workspace.dependencies] # RustFS Internal Crates -rc-core = { path = "./crates/core", version = "0.1.24" } -rc-s3 = { path = "./crates/s3", version = "0.1.24" } -rustfs-cli = { path = "./crates/cli", version = "0.1.24" } +rc-core = { path = "./crates/core", version = "0.1.37" } +rc-s3 = { path = "./crates/s3", version = "0.1.37" } +rustfs-cli = { path = "./crates/cli", version = "0.1.37" } # Async runtime tokio = { version = "1.49", features = ["full"] } From 82a79b6f171ef81362912c54fe7cbcbf54c582f4 Mon Sep 17 00:00:00 2001 From: overtrue Date: Tue, 3 Nov 2026 08:37:38 +0800 Subject: [PATCH 2/3] feat(phase-2): fix manual release tags and HEAD test responses Signed-off-by: overtrue --- .github/workflows/docker.yml | 8 +++- crates/cli/tests/recursive_remote_copy.rs | 46 ++++++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e4f003d..8d2c804 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,6 +10,11 @@ on: description: 'Image tag to build and push' required: true default: 'latest' + publish_latest: + description: 'Also update latest for a stable version tag' + required: false + type: boolean + default: false permissions: contents: read @@ -40,6 +45,7 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} MANUAL_TAG: ${{ github.event.inputs.tag }} + PUBLISH_LATEST: ${{ github.event.inputs.publish_latest }} shell: bash run: | set -euo pipefail @@ -58,7 +64,7 @@ jobs: { echo 'tags<) -> Option PathBuf { if let Ok(path) = std::env::var("CARGO_BIN_EXE_rc") { return PathBuf::from(path); From 79e0e70e8fb2fe2a8aef499bc334975c572a8d65 Mon Sep 17 00:00:00 2001 From: overtrue Date: Tue, 3 Nov 2026 08:48:29 +0800 Subject: [PATCH 3/3] feat(phase-2): keep credentials out of dependency debug logs Signed-off-by: overtrue --- crates/cli/src/launcher.rs | 41 ++++++++++- crates/cli/tests/session_credentials.rs | 93 ++++++++++++++----------- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/crates/cli/src/launcher.rs b/crates/cli/src/launcher.rs index 97df567..178b287 100644 --- a/crates/cli/src/launcher.rs +++ b/crates/cli/src/launcher.rs @@ -4,7 +4,7 @@ //! Designed for RustFS and other S3-compatible backends. use clap::Parser; -use tracing_subscriber::{EnvFilter, fmt, prelude::*}; +use tracing_subscriber::{EnvFilter, filter::filter_fn, fmt, prelude::*}; use crate::commands::{self, Cli}; use crate::exit_code::ExitCode; @@ -34,6 +34,15 @@ fn build_env_filter(debug: bool, env_is_set: bool) -> EnvFilter { } } +fn allow_log_metadata(metadata: &tracing::Metadata<'_>) -> bool { + // SDK debug events can include credentials and signed request headers. + *metadata.level() <= tracing::Level::WARN + || matches!( + metadata.target().split("::").next(), + Some("rustfs_cli" | "rc_core" | "rc_s3") + ) +} + /// Run the shared CLI launcher, preserving its worker-thread and exit behavior. pub fn run() { run_with_shutdown(|| async {}); @@ -52,6 +61,7 @@ where // Initialize tracing subscriber for logging tracing_subscriber::registry() + .with(filter_fn(allow_log_metadata)) .with(fmt::layer()) .with(env_filter) .init(); @@ -158,6 +168,35 @@ mod tests { assert_eq!(filter.to_string(), "debug"); } + #[test] + fn dependency_debug_logs_are_capped_without_hiding_application_diagnostics() { + let subscriber = tracing_subscriber::registry() + .with(filter_fn(allow_log_metadata)) + .with(EnvFilter::new("trace")); + tracing::subscriber::with_default(subscriber, || { + assert!(tracing::enabled!( + target: "rc_s3::client", + tracing::Level::DEBUG + )); + assert!(tracing::enabled!( + target: "rustfs_cli::launcher", + tracing::Level::TRACE + )); + assert!(tracing::enabled!( + target: "aws_smithy_runtime::client", + tracing::Level::WARN + )); + assert!(!tracing::enabled!( + target: "aws_smithy_runtime::client", + tracing::Level::DEBUG + )); + assert!(!tracing::enabled!( + target: "reqwest::connect", + tracing::Level::TRACE + )); + }); + } + #[test] fn command_launcher_preserves_command_exit_code() { let exit_code = run_async_command(|| async { ExitCode::UnsupportedFeature }, test_runtime) diff --git a/crates/cli/tests/session_credentials.rs b/crates/cli/tests/session_credentials.rs index d0f72e5..bcb47ed 100644 --- a/crates/cli/tests/session_credentials.rs +++ b/crates/cli/tests/session_credentials.rs @@ -9,7 +9,12 @@ const TOKEN: &str = "temporary-TOKEN+/="; const LIST: &str = "bucketfalse"; -fn run(endpoint: &str, config: &std::path::Path, args: &[&str]) -> Output { +fn run( + endpoint: &str, + config: &std::path::Path, + args: &[&str], + log_filter: Option<&str>, +) -> Output { let mut command = Command::new(rc_binary()); for (key, _) in std::env::vars_os() { if key.to_string_lossy().starts_with("RC_HOST_") { @@ -19,13 +24,16 @@ fn run(endpoint: &str, config: &std::path::Path, args: &[&str]) -> Output { let authority = endpoint.strip_prefix("http://").unwrap(); command .args(args) + .env_remove("RUST_LOG") .env("RC_CONFIG_DIR", config) .env("AWS_EC2_METADATA_DISABLED", "true") .env("RC_HOST_temporary", format!( "http://temporary-access:temporary-secret@{authority}?session_token=temporary-TOKEN%2B%2F%3D" - )) - .output() - .expect("run rc") + )); + if let Some(log_filter) = log_filter { + command.env("RUST_LOG", log_filter); + } + command.output().expect("run rc") } fn assert_redacted(output: &Output) { @@ -41,41 +49,48 @@ fn assert_redacted(output: &Output) { #[test] fn s3_and_admin_sign_with_the_same_process_only_session_token() { - for (args, body) in [ - (vec!["--debug", "ls", "temporary/bucket"], LIST), - (vec!["--debug", "admin", "user", "list", "temporary"], "{}"), + for log_filter in [ + None, + Some("debug"), + Some("trace"), + Some("aws_smithy_runtime::client::orchestrator=trace"), ] { - let config = tempfile::tempdir().unwrap(); - let (endpoint, requests, server) = - start_admin_response_test_server("200 OK", "application/xml", body.into()); - let output = run(&endpoint, config.path(), &args); - assert!( - output.status.success(), - "{}", - String::from_utf8_lossy(&output.stderr) - ); - assert_redacted(&output); - let request = requests - .recv_timeout(std::time::Duration::from_secs(2)) - .unwrap(); - assert!( - request.headers.lines().any(|line| { - line.split_once(':').is_some_and(|(name, value)| { - name.eq_ignore_ascii_case("x-amz-security-token") && value.trim() == TOKEN - }) - }), - "{}", - request.headers - ); - assert!( - request - .headers - .to_ascii_lowercase() - .contains("signedheaders=") - ); - assert!(!request.target.contains("session_token")); - assert!(!config.path().join("config.toml").exists()); - server.join().unwrap(); + for (args, body) in [ + (vec!["--debug", "ls", "temporary/bucket"], LIST), + (vec!["--debug", "admin", "user", "list", "temporary"], "{}"), + ] { + let config = tempfile::tempdir().unwrap(); + let (endpoint, requests, server) = + start_admin_response_test_server("200 OK", "application/xml", body.into()); + let output = run(&endpoint, config.path(), &args, log_filter); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_redacted(&output); + let request = requests + .recv_timeout(std::time::Duration::from_secs(2)) + .unwrap(); + assert!( + request.headers.lines().any(|line| { + line.split_once(':').is_some_and(|(name, value)| { + name.eq_ignore_ascii_case("x-amz-security-token") && value.trim() == TOKEN + }) + }), + "{}", + request.headers + ); + assert!( + request + .headers + .to_ascii_lowercase() + .contains("signedheaders=") + ); + assert!(!request.target.contains("session_token")); + assert!(!config.path().join("config.toml").exists()); + server.join().unwrap(); + } } } @@ -101,7 +116,7 @@ fn expired_or_invalid_tokens_return_auth_error_without_changing_aliases() { } else { vec!["--json", "ls", "temporary/bucket"] }; - let output = run(&endpoint, config.path(), &args); + let output = run(&endpoint, config.path(), &args, None); assert_eq!( output.status.code(), Some(4),