feat: micro backend modules, new crates, and expanded test coverage#4
Conversation
…nd event logging - Added workspace crates section to README.md for better navigation. - Updated shipper-cli's Cargo.toml to improve description and documentation links. - Enhanced CLI argument parsing to support global flags for various commands. - Implemented authentication type detection in the shipper library, distinguishing between token-based and trusted publishing environments. - Introduced event logging for preflight checks, capturing significant events in a JSONL format. - Updated tests to validate new authentication detection and event logging features. - Created detailed README.md files for both shipper and shipper-cli crates. - Added a comprehensive Handoff document outlining repository state, architecture, and next actions.
… and security policies
… performance goals, and ecosystem integration
- Introduced a new module for retry strategies, allowing users to customize retry behavior based on error types. - Implemented various retry strategies: Immediate, Exponential, Linear, and Constant. - Added predefined retry policies: Default, Aggressive, Conservative, and Custom. - Created configuration structures for retry strategies and per-error configurations. - Implemented delay calculation logic with support for jitter. - Added tests for retry policies, delay calculations, and configuration serialization. feat: implement webhook notifications for publish events - Added a new module for webhook functionality to send HTTP POST callbacks on publish events. - Configurable webhook settings including URL, secret, and timeout. - Defined events that can trigger webhook notifications. - Implemented a webhook client for sending events asynchronously. - Added tests for webhook configuration, payload serialization, and event handling. ci: add Azure DevOps and CircleCI configurations for CI/CD - Created Azure DevOps pipeline configuration for continuous integration and optional publishing to crates.io. - Added CircleCI configuration for CI/CD with build, test, linting, and optional publishing jobs. - Both configurations include caching of cargo dependencies and environment variable setups for Rust toolchain.
- Added `StorageBackend` trait for unified storage operations. - Implemented `FileStorage` for local filesystem storage. - Created `S3Storage`, `GcsStorage`, and `AzureStorage` modules for respective cloud services. - Introduced `CloudStorageConfig` for configuration management. - Added environment variable support for storage configuration. - Included tests for file storage operations and cloud storage configuration.
- Reorganized imports in config.rs for clarity. - Simplified environment variable overrides in StorageConfigInner. - Enhanced readability of ShipperConfig initialization by formatting long lines. - Improved code structure in encryption.rs by consolidating error handling and formatting. - Streamlined the calculation of publish completion statistics in engine.rs. - Updated retry.rs to enhance clarity in delay calculation logic. - Refactored state.rs to improve function signatures and readability. - Enhanced storage.rs by simplifying error context messages and improving formatting. - Cleaned up types.rs by removing unnecessary whitespace and improving comments. - Improved webhook.rs by consolidating error handling and enhancing readability.
Remove non-functional S3/GCS/Azure storage implementations that used incorrect SDK APIs, along with a 3581-line generated BDD test file with 40+ clippy violations. Keep the StorageBackend trait, FileStorage, and config types. Fix clippy warnings in encryption tests and stale doc tests in engine/types/lib.
- Introduced property tests for package state serialization, registry normalization, delay bounds, and error class roundtrips. - Implemented stress tests to evaluate performance under high load, including lock acquisition, state save/load cycles, and large publish levels. - Created feature files for preflight checks and resumable publishing scenarios, ensuring robust validation before publishing. - Added fuzz targets for encryption/decryption, retry strategy calculations, and types serialization to enhance input robustness. - Documented testing strategies and guidelines in a new testing guide.
- Introduced `shipper-store` crate for state store abstraction, including schema versioning and file-based storage. - Added `shipper-types` crate containing core types for error classification, registry configuration, and publish policies. - Implemented `shipper-webhook` crate for sending webhook notifications to Slack and Discord, with support for customizable payloads. - Each crate includes comprehensive documentation and example usage.
…crypt, environment, and storage - Added `auth_micro.rs` for handling authentication tokens and types. - Introduced `cargo_micro.rs` for cargo-related functionalities. - Created `encryption_micro.rs` for encryption utilities. - Implemented `environment_micro.rs` for environment fingerprinting. - Added `events_micro.rs` for event logging functionalities. - Developed `git_micro.rs` for collecting git context information. - Created `lock_micro.rs` for lock management. - Introduced `storage_micro.rs` for storage backend functionalities. - Added feature flags for micro backend modules in `micro_backend_feature_flags.feature`. - Ensured stable preflight behavior with micro backends enabled.
- Moved cargo command execution logic to a new `process` module for better organization and reusability. - Updated `cargo_publish`, `cargo_publish_dry_run_workspace`, and `cargo_publish_dry_run_package` functions to utilize the new `run_command_with_timeout` function from the `process` module. - Enhanced error handling and output management during command execution. - Introduced new micro modules for `config`, `process`, `registry`, `state`, `store`, `types`, and `webhook` to support modular feature flags. - Updated CircleCI configuration and documentation to reflect new feature flags and testing commands. - Improved code readability and consistency across various modules.
…allow(dead_code)]
Summary by CodeRabbit
WalkthroughAdds a modular workspace with 16+ new crates (auth, config, crypto, registry, state, storage, retry, webhook, events, etc.), extensive CLI and engine changes (preflight, publish, resume, progress, completions), CI/Dependabot workflows, docs (CONTRIBUTING, ROADMAP, SECURITY), and many tests and manifests. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI as Shipper CLI
participant Engine as Engine (run_publish)
participant Lock as LockFile
participant State as StateStore
participant Registry as RegistryClient
participant Webhook as WebhookService
participant Storage as StorageBackend
User->>CLI: shipper publish (flags/config)
CLI->>Engine: build plan & runtime options
Engine->>Lock: acquire(state_dir)
Lock-->>Engine: lock acquired
Engine->>State: load_state_encrypted? / init state
Engine->>Registry: for each package: dry-run / version checks / owners
Registry-->>Engine: ownership/visibility results
Engine->>Registry: publish package (cargo_publish)
Registry-->>Engine: publish result
Engine->>State: save progress / write receipt / append events
Engine->>Webhook: emit PublishStarted/PackagePublished/PublishCompleted
Engine->>Storage: upload artifacts / receipts (if configured)
Engine->>Lock: release()
Engine-->>CLI: final receipt / exit
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @EffortlessSteven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors the project's architecture by introducing a modular micro-backend approach, allowing for greater flexibility and reduced dependency footprints. It also bolsters the project's reliability through extensive new testing methodologies, including property and stress tests, and enhances the robustness of preflight validations. These changes collectively improve the maintainability, extensibility, and overall stability of the publishing process. Highlights
Changelog
Ignored Files
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a modular architecture with fine-grained feature gates, adds new microcrates for composability, and significantly expands test coverage with property-based and stress testing.
Changes:
- Micro backend modules with
micro-*features for granular dependency control - New workspace crates:
shipper-store,shipper-types,shipper-webhook, and 12 other microcrates - Property tests using
proptestand concurrency stress tests for core data structures - Process module refactor extracting cargo execution logic for better separation
- HMAC webhook signature support and expanded preflight validation tests
Reviewed changes
Copilot reviewed 73 out of 96 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
crates/shipper/src/registry_micro.rs |
Registry client with exponential backoff and readiness verification |
crates/shipper/src/property_tests.rs |
Property-based tests for state serialization, topological ordering, and retry delays |
crates/shipper/src/process_micro.rs |
Thin wrapper delegating to shipper-process microcrate |
crates/shipper/src/process.rs |
Command execution with timeout support and output capture |
crates/shipper/src/plan.rs |
Feature-gated metadata loading and type path adjustments |
crates/shipper/src/lib.rs |
Feature-gated module paths for micro backend architecture |
crates/shipper/src/git_micro.rs |
Git operations with environment variable override support |
crates/shipper/src/git.rs |
Removed invalid arg and added real git tag test |
crates/shipper/src/encryption.rs |
AES-256-GCM encryption with PBKDF2 key derivation |
crates/shipper/src/cargo.rs |
Refactored to use process module for command execution |
crates/shipper/src/auth.rs |
Added detect_auth_type for trusted publishing detection |
crates/shipper/Cargo.toml |
Added microcrate dependencies and feature definitions |
crates/shipper-webhook/src/lib.rs |
Webhook notifications with HMAC signature support |
crates/shipper-process/src/lib.rs |
Cross-platform process execution utilities |
crates/shipper-cli/Cargo.toml |
Added progress bar dependencies and micro feature pass-through |
crates/shipper-cli/src/progress.rs |
TTY-aware progress reporting with fallback to simple output |
Cargo.toml |
Added 14 new workspace members for microcrates |
.github/workflows/ci.yml |
Expanded CI with BDD matrix, coverage, fuzz, and cross-platform builds |
README.md |
Updated with workspace crate overview and auth type documentation |
| #[allow(dead_code)] | ||
| #[derive(Debug, Clone)] | ||
| pub(crate) struct CommandOutput { |
There was a problem hiding this comment.
The #[allow(dead_code)] attribute is applied at the struct level. According to the coding guidelines, test-only items should be scoped under #[cfg(test)] instead of using blanket #[allow(dead_code)]. Since this struct appears to be used in production code (called from cargo.rs), this attribute may be unnecessary.
There was a problem hiding this comment.
Feature-dead by design under the matrix; not test-only; clippy clean under .
| pub(crate) stdout: String, | ||
| pub(crate) stderr: String, | ||
| pub(crate) timed_out: bool, | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Individual struct field has #[allow(dead_code)] attribute. Per the coding guidelines, prefer #[cfg(test)] for test-only items rather than blanket dead code allowances. If this field is genuinely used in production code, the attribute should be removed.
| #[allow(dead_code)] |
| #[allow(dead_code)] | ||
| #[derive(Debug, Clone)] | ||
| pub(crate) struct CommandOutput { |
There was a problem hiding this comment.
Using #[allow(dead_code)] instead of #[cfg(test)] for test-only code. The coding guidelines specify that test-only items should be scoped under #[cfg(test)] rather than using blanket allowances.
| #[allow(dead_code)] | ||
| pub(crate) fn run_command_with_timeout( |
There was a problem hiding this comment.
Function marked with #[allow(dead_code)]. According to the coding guidelines, test-only functions should use #[cfg(test)] instead of blanket dead code allowances. If this function is used in production, the attribute should be removed.
| .saturating_mul(2_u32.saturating_pow(attempt.saturating_sub(2).min(16))); | ||
| let capped_delay = exponential_delay.min(config.max_delay); | ||
| let jitter_range = config.jitter_factor; | ||
| let jitter = 1.0 + (rand::random::<f64>() * 2.0 * jitter_range - jitter_range); |
There was a problem hiding this comment.
This jitter calculation is duplicated at lines 143, 189, and 213. Consider extracting this into a helper function to avoid code duplication and improve maintainability.
| mod tests { | ||
| use std::fs; | ||
| use std::path::{Path, PathBuf}; | ||
| use std::process::Command; |
There was a problem hiding this comment.
Import added in the test module but Command is already imported at the module level (line 3). This creates a redundant import that should be removed.
| use std::process::Command; |
|
|
||
| /// Creates a silent progress reporter that always uses non-TTY mode. | ||
| /// Use this when you explicitly want to disable progress bars regardless of TTY. | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Function marked with #[allow(dead_code)] but is used in tests. According to the coding guidelines, test-only code should be scoped under #[cfg(test)] instead of using blanket dead code allowances.
| #[allow(dead_code)] | |
| #[cfg(test)] |
|
|
||
| /// Marks the current package as completed. | ||
| #[allow(clippy::collapsible_if)] | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Multiple allow attributes stacked on this function. The #[allow(dead_code)] should be replaced with #[cfg(test)] if this is test-only code, per the coding guidelines.
| #[allow(dead_code)] | |
| #[cfg(test)] |
| #[allow(dead_code)] | ||
| pub fn set_status(&self, status: &str) { |
There was a problem hiding this comment.
Function has #[allow(dead_code)] attribute. According to the coding guidelines, test-only items should use #[cfg(test)] instead of blanket dead code allowances.
| license.workspace = true | ||
| rust-version.workspace = true | ||
| description = "Retry strategies and backoff policies for shipper" | ||
| repository = "https://github.com/cmrigney/shipper" |
There was a problem hiding this comment.
Repository URL uses different GitHub username (cmrigney) compared to other microcrates which use EffortlessMetrics. This inconsistency should be corrected for uniformity across the workspace.
| repository = "https://github.com/cmrigney/shipper" | |
| repository = "https://github.com/EffortlessMetrics/shipper" |
There was a problem hiding this comment.
💡 Codex Review
shipper/crates/shipper-cli/src/main.rs
Lines 332 to 334 in 0f2021b
The registry from .shipper.toml is applied only after plan::build_plan has already run, so when users rely on config-only registry settings (no --registry flag), package filtering and dependency validation are still done against the default crates-io value. This can silently drop crates whose publish list only allows the configured registry, producing an incomplete or empty publish plan even though planned.plan.registry is later rewritten.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| [dependencies] | ||
| # Workspace microcrates | ||
| shipper-retry = { path = "../shipper-retry" } |
There was a problem hiding this comment.
Add versions for publishable path dependencies
This crate now declares internal dependencies as path-only entries, but the release workflow publishes shipper to crates.io (.github/workflows/release.yml), and published manifests must reference registry versions for dependencies. With path-only deps, cargo publish -p shipper will fail at release time, blocking crate publication.
Useful? React with 👍 / 👎.
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment Thanks for integrating Codecov - We've got you covered ☂️ |
There was a problem hiding this comment.
Code Review
This pull request introduces significant architectural changes by modularizing the backend into micro-crates, expanding test coverage, and adding new features like shell completions and webhook support. The changes improve maintainability, testability, and provide a clearer roadmap for future development. The addition of CONTRIBUTING.md, ROADMAP.md, and SECURITY.md also greatly enhances project documentation and governance. Overall, the changes are well-structured and align with the project's goals of reliability and transparency.
| // Ensure only one default registry | ||
| let default_count = self | ||
| .registries | ||
| .registries | ||
| .iter() | ||
| .filter(|r| r.default) | ||
| .count(); | ||
| if default_count > 1 { | ||
| bail!("only one registry can be marked as default"); | ||
| } |
| } | ||
| } | ||
| eprintln!("[warn] Use --force to override the lock"); | ||
| bail!("cannot clean: active lock exists"); |
There was a problem hiding this comment.
| // Validate jitter | ||
| if self.retry.jitter < 0.0 || self.retry.jitter > 1.0 { | ||
| bail!("retry.jitter must be between 0.0 and 1.0"); |
| // Validate multiple registries if present | ||
| for reg in &self.registries.registries { | ||
| if reg.name.is_empty() { | ||
| bail!("registries[].name cannot be empty"); | ||
| } | ||
| if reg.api_base.is_empty() { | ||
| bail!("registries[].api_base cannot be empty"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 113
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
RELEASE_CHECKLIST_v0.2.0.md (1)
63-67:⚠️ Potential issue | 🟡 MinorPublish commands don't cover the new workspace crates.
Multiple new crates introduced in this PR (
shipper-types,shipper-environment,shipper-events,shipper-storage,shipper-registry,shipper-retry,shipper-webhook, etc.) each declare adocumentation = "https://docs.rs/..."URL, indicating intent to publish. They are absent from the publish block. If these crates are meant to be published as part of v0.2.0, the block needs to be expanded — and in dependency order (leaves first, e.g.shipper-typesbefore any crate that depends on it).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@RELEASE_CHECKLIST_v0.2.0.md` around lines 63 - 67, The publish checklist only lists shipper and shipper-cli but omits the new workspace crates; update the "Publish to crates.io" block to include all new crates (at least shipper-types, shipper-environment, shipper-events, shipper-storage, shipper-registry, shipper-retry, shipper-webhook, etc.) in the correct dependency order (publish leaf crates like shipper-types first, then crates that depend on them, ending with shipper and shipper-cli); ensure each crate appears as a separate cargo publish -p <crate-name> entry and order them so dependents come after their dependencies.crates/shipper/src/config.rs (1)
864-1221: 🧹 Nitpick | 🔵 TrivialTests use plain assertions instead of
instasnapshot testing.The coding guidelines recommend using
instafor snapshot testing. While the current assertions are correct, consider snapshot-testing the TOML template output and theRuntimeOptionsproduced bybuild_runtime_optionsto improve regression detection as the config surface grows.As per coding guidelines, "Use snapshot testing with
instafor test assertions."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/config.rs` around lines 864 - 1221, Replace plain assert-based checks in the tests (notably test_parse_toml_config, test_parse_toml_with_registry, test_parse_toml_with_parallel, test_parse_toml_with_partial_*, and the test_build_runtime_options* tests) with insta snapshot assertions: serialize the parsed ShipperConfig/TOML output and the RuntimeOptions returned by ShipperConfig::build_runtime_options (use serde_json or Debug) and call insta::assert_snapshot! (or assert_json_snapshot!) to capture expected output; update test names/fixtures accordingly and add/modify the test module to initialize insta (if needed) so snapshots are created/checked instead of using multiple assert_eq!/assert! calls.crates/shipper-cli/tests/cli_e2e.rs (2)
906-941: 🧹 Nitpick | 🔵 TrivialRedundant parentheses in
vec!literal.
vec![(404), (404)]— the parens around the integer literals are unnecessary.Nit
let registry = spawn_registry( - vec![ - (404), // crate check - (404), // version check - ], + vec![404, 404], 2, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper-cli/tests/cli_e2e.rs` around lines 906 - 941, In the test preflight_command_with_new_crates replace the vec![(404), (404)] with a plain vec![404, 404] (remove the unnecessary parentheses around the integer literals) so the registry mock invocation uses a normal vec! literal; update the occurrence in the spawn_registry(...) call to reference vec![404, 404] accordingly.
129-149:⚠️ Potential issue | 🟡 Minor
spawn_registryserver thread can hang if a test exits early.The server thread blocks on
server.recv()for exactlyexpected_requestsiterations. If a test assertion fails or panics before all requests are sent,registry.join()(or the implicit thread destructor) will hang. Consider adding a short receive timeout or switching to atry_recv-style pattern with a shutdown signal.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper-cli/tests/cli_e2e.rs` around lines 129 - 149, The spawned server thread in spawn_registry blocks forever on server.recv() if the test panics or exits early; change the request loop to use a timed receive (e.g., server.recv_timeout(Duration::from_millis(...)) or a try_recv pattern) and handle the timeout by breaking out of the loop and exiting the thread so TestRegistry.handle.join() cannot hang; update spawn_registry to treat a timeout as a signal to stop (while still honoring expected_requests when requests arrive) and ensure any Err from recv_timeout is handled gracefully so the thread always terminates.crates/shipper/src/cargo.rs (1)
100-146: 🧹 Nitpick | 🔵 TrivialMinor:
CargoOutput.durationis measured independently ofCommandOutput.duration.
start.elapsed()at line 137 captures wall-clock time from the caller's perspective, whileprocess::run_command_with_timeoutalso records its owndurationinternally (which is discarded here). This is fine if the intent is caller-perspective timing, but it does mean the duration includes the overhead of building the args vector and context wrapping. If you ever need process-only duration, consider usingoutput.durationinstead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/cargo.rs` around lines 100 - 146, The CargoOutput.duration in cargo_publish is currently set from start.elapsed() (measuring caller-side wall time) while process::run_command_with_timeout already returns a process-only duration on the returned CommandOutput; swap to use the returned output.duration (from the value returned by process::run_command_with_timeout) when constructing CargoOutput so CargoOutput.duration reflects the process execution time, or explicitly document/choose which timing is desired in cargo_publish.crates/shipper/src/plan.rs (1)
324-377: 🧹 Nitpick | 🔵 Trivial
group_by_levelsis duplicated betweenplan.rsandshipper-types.This implementation (lines 324–377) is nearly identical to the one in
crates/shipper-types/src/lib.rs:695-732. The#[cfg(not(feature = "micro-types"))]gate ensures only one is compiled, but having two copies to maintain is a code smell. Consider extracting the algorithm into a shared free function that both sites call, or consolidating it in one location with a re-export.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/plan.rs` around lines 324 - 377, The ReleasePlan::group_by_levels implementation is duplicated; extract the leveling algorithm into a single shared free function (e.g., group_packages_by_levels) that takes the plan inputs (packages: &[Package], dependencies: &HashMap<String, Vec<String>>) and returns Vec<crate::types::PublishLevel>, then replace the ReleasePlan::group_by_levels definition to call that shared function; ensure the shared function lives in a common crate (preferably shipper-types) and is re-exported or called from both crates so only one implementation of the algorithm remains and ReleasePlan (and any other callers) delegate to it.crates/shipper/src/engine_parallel.rs (1)
694-715: 🛠️ Refactor suggestion | 🟠 MajorUnnecessary double-locking:
SendReporterfields areMutex<Vec<String>>but the struct is already behindArc<Mutex<…>>.The outer
Mutexgrants exclusive&mut selfaccess. The innerMutexon each field is redundant and adds unnecessary synchronization overhead.Proposed fix
struct SendReporter { - infos: Mutex<Vec<String>>, - warns: Mutex<Vec<String>>, - errors: Mutex<Vec<String>>, + infos: Vec<String>, + warns: Vec<String>, + errors: Vec<String>, } impl Reporter for SendReporter { fn info(&mut self, msg: &str) { - self.infos.lock().unwrap().push(msg.to_string()); + self.infos.push(msg.to_string()); } fn warn(&mut self, msg: &str) { - self.warns.lock().unwrap().push(msg.to_string()); + self.warns.push(msg.to_string()); } fn error(&mut self, msg: &str) { - self.errors.lock().unwrap().push(msg.to_string()); + self.errors.push(msg.to_string()); } } let send_reporter = Arc::new(Mutex::new(SendReporter { - infos: Mutex::new(Vec::new()), - warns: Mutex::new(Vec::new()), - errors: Mutex::new(Vec::new()), + infos: Vec::new(), + warns: Vec::new(), + errors: Vec::new(), }));Also update the replay section:
{ let sr = send_reporter.lock().unwrap(); - for msg in sr.infos.lock().unwrap().iter() { + for msg in sr.infos.iter() { reporter.info(msg); } - for msg in sr.warns.lock().unwrap().iter() { + for msg in sr.warns.iter() { reporter.warn(msg); } - for msg in sr.errors.lock().unwrap().iter() { + for msg in sr.errors.iter() { reporter.error(msg); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/engine_parallel.rs` around lines 694 - 715, The SendReporter struct currently wraps its Vec<String> fields in Mutex while the entire struct is already stored in an Arc<Mutex<...>>, causing redundant locking; change SendReporter::infos, warns, errors from Mutex<Vec<String>> to plain Vec<String>, update the impl Reporter methods (info/warn/error) to push directly on &mut self (no inner locks), and update the send_reporter construction to initialize plain Vecs inside Arc::new(Mutex::new(SendReporter { infos: Vec::new(), warns: Vec::new(), errors: Vec::new() })); also propagate the same removal of inner Mutexes to the replay section mentioned so usages assume outer Mutex protection.crates/shipper-cli/src/main.rs (1)
426-445:⚠️ Potential issue | 🟡 MinorProgressReporter updates are incomplete—only the first package is displayed, with no progress tracking during the publish loop.
ProgressReporteris created andset_package()is called once for the first package beforeengine::run_publish()executes. However,engine::run_publish()receives theCliReporter(which implements theReportertrait), notProgressReporter. Theprogressobject is then abandoned during the publish loop andfinish()is called after completion. Thefinish_package()method exists but is never invoked in production code—it appears only in unit tests.To properly track progress, either:
- Pass
progressto the engine and callprogress.set_package()andprogress.finish_package()for each package in the publish loop, or- Implement
ReporteronProgressReporterand use it instead ofCliReporter🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper-cli/src/main.rs` around lines 426 - 445, Progress reporting is incomplete because a CliReporter is passed into engine::run_publish while ProgressReporter is created but never used; implement the Reporter trait for ProgressReporter (implementing the same callbacks used by the engine such as package start/finish/status updates) and then pass &mut progress into engine::run_publish instead of &mut reporter so the publish loop drives ProgressReporter.set_package(...) and finish_package(...) automatically; keep the final progress.finish() call and remove or consolidate the manual initial set_package() if that becomes redundant once Reporter methods are wired up.crates/shipper/src/engine.rs (2)
2409-2525:⚠️ Potential issue | 🟡 MinorAdd
#[serial]to filesystem-mutating tests.These tests create tempdirs and write state files but aren’t serialized, which can introduce cross-test interference on shared FS state.
✅ Suggested fix (apply to all listed tests)
#[test] +#[serial] fn run_publish_errors_on_plan_mismatch_without_force_resume() { #[test] +#[serial] fn run_publish_allows_forced_resume_with_plan_mismatch() { #[test] +#[serial] fn run_resume_errors_when_state_is_missing() { #[test] +#[serial] fn run_resume_runs_publish_when_state_exists() {As per coding guidelines, Use
#[serial]attribute fromserial_testfor tests that mutate global environment or filesystem state.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/engine.rs` around lines 2409 - 2525, Add the #[serial] attribute (from the serial_test crate) to each filesystem-mutating test to prevent cross-test interference: apply it to the test functions run_publish_errors_on_plan_mismatch_without_force_resume, run_publish_allows_forced_resume_with_plan_mismatch, run_resume_errors_when_state_is_missing, and run_resume_runs_publish_when_state_exists; also ensure the serial_test dependency is in Cargo.toml and the serial attribute is imported (e.g., use serial_test::serial) where these tests live.
670-678:⚠️ Potential issue | 🟠 MajorRecord success when the registry confirms publish after a cargo failure.
When
cargo publishfails butversion_existsreturns true, the package is markedPublishedwithout aPackagePublishedevent orPublishSucceededwebhook, leaving audit/webhook consumers blind to the success.✅ Suggested fix
if reg.version_exists(&p.name, &p.version)? { reporter.info(&format!( "{}@{}: version is present on registry; treating as published", p.name, p.version )); update_state(&mut st, &state_dir, &key, PackageState::Published)?; + + event_log.record(PublishEvent { + timestamp: Utc::now(), + event_type: EventType::PackagePublished { + duration_ms: start_instant.elapsed().as_millis() as u64, + }, + package: pkg_label.clone(), + }); + event_log.write_to_file(&events_path)?; + event_log.clear(); + + webhook::maybe_send_event( + &opts.webhook, + WebhookEvent::PublishSucceeded { + plan_id: ws.plan.plan_id.clone(), + package_name: p.name.clone(), + package_version: p.version.clone(), + duration_ms: start_instant.elapsed().as_millis() as u64, + }, + ); + last_err = None; break; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/engine.rs` around lines 670 - 678, When reg.version_exists(&p.name, &p.version) returns true after a failed cargo publish, we currently call update_state(&mut st, &state_dir, &key, PackageState::Published) and clear last_err but do not emit the PackagePublished event or trigger the PublishSucceeded webhook; modify the branch handling that condition to replicate the successful-publish path: invoke the same event emission (e.g., emit or record a PackagePublished event) and call the PublishSucceeded webhook handler used elsewhere so audit logs and webhook consumers see the success, keeping update_state, clearing last_err, and preserving any retry/metrics logic around functions like reg.version_exists, update_state, PackageState::Published, last_err, and whatever function emits webhooks/events.
| archive: zip | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
Pin all Actions to their full commit SHA to prevent supply chain attacks.
This workflow publishes crates to crates.io and holds CARGO_REGISTRY_TOKEN. Mutable tags (@v4, @v2, @stable) can be silently repointed to malicious commits, allowing an attacker to exfiltrate the token or push a compromised crate.
🔒 Example for `actions/checkout@v4`
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Apply the same pattern (SHA + version comment) to dtolnay/rust-toolchain@stable, actions/cache@v4, actions/upload-artifact@v4, actions/download-artifact@v4, and softprops/action-gh-release@v2.
Also applies to: 43-43, 48-48, 75-75, 89-89, 97-97, 113-113, 116-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml at line 40, Replace mutable action version
tags with their corresponding full commit SHAs and keep a trailing comment with
the human-friendly version (e.g., replace actions/checkout@v4 with
actions/checkout@<full-sha> # v4); do this for the occurrences mentioned
(actions/checkout, dtolnay/rust-toolchain@stable, actions/cache@v4,
actions/upload-artifact@v4, actions/download-artifact@v4,
softprops/action-gh-release@v2 and any other mutable tags on lines noted) so the
workflow pins each uses: entry to an exact commit SHA while preserving the
original tag as a comment for clarity.
| publish-crates-io: | ||
| name: Publish to crates.io | ||
| runs-on: ubuntu-latest | ||
| needs: create-release | ||
| environment: release |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Release can run without CI passing — add a CI workflow dependency.
The publish-crates-io job only needs: create-release, which itself only needs: build-binaries. There is no requirement that the main CI workflow (tests, clippy, MSRV) passed on the tagged commit. A maintainer who force-pushes a tag on an untested commit will publish broken crates.
♻️ Suggested guard
If the CI workflow is in .github/workflows/ci.yml, reference it via workflow_run or require the ci status check in branch/tag protection rules. Alternatively restructure as a single workflow:
publish-crates-io:
needs: [ci, create-release] # "ci" must be a reusable/called workflow job🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 104 - 108, The publish-crates-io
job currently only depends on create-release; update the workflow so publishing
requires the main CI to have passed by adding the CI job as a dependency or
using a workflow_run guard: modify the publish-crates-io job (symbol:
publish-crates-io) to include needs: [create-release, ci] where "ci" is the main
CI job/workflow (or configure a workflow_run trigger to only run after the ci
workflow completes successfully), ensuring the publish step will not run unless
CI (tests/clippy/MSRV) has passed for the tagged commit.
| publish-crates-io: | ||
| name: Publish to crates.io | ||
| runs-on: ubuntu-latest | ||
| needs: create-release | ||
| environment: release | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
|
|
||
| - name: Cache Cargo | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-publish-${{ hashFiles('**/Cargo.lock') }} | ||
| restore-keys: ${{ runner.os }}-publish- | ||
|
|
||
| - name: Publish shipper crate | ||
| run: cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }} | ||
| continue-on-error: true # May already be published | ||
|
|
||
| - name: Wait for index update | ||
| run: sleep 30 | ||
|
|
||
| - name: Publish shipper-cli crate | ||
| run: cargo publish -p shipper-cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }} No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# List all workspace crate manifests and their publish fields
fd "Cargo.toml" crates/ --exec grep -l '^\[package\]' 2>/dev/null | sort | xargs -I{} sh -c \
'echo "=== {} ==="; grep -E "^name|^publish" {}'Repository: EffortlessMetrics/shipper
Length of output: 1233
🏁 Script executed:
#!/bin/bash
# Check publish field in all crates
for file in crates/*/Cargo.toml; do
echo "=== $file ==="
sed -n '/^\[package\]/,/^\[/p' "$file" | grep -E "^name|^publish" | head -2
doneRepository: EffortlessMetrics/shipper
Length of output: 1216
🏁 Script executed:
#!/bin/bash
# Check a few representative Cargo.toml files
for file in crates/shipper/Cargo.toml crates/shipper-cli/Cargo.toml crates/shipper-lock/Cargo.toml crates/shipper-types/Cargo.toml; do
echo "=== $file ==="
sed -n '/^\[package\]/,/^\[/p' "$file" | head -20
doneRepository: EffortlessMetrics/shipper
Length of output: 2140
🏁 Script executed:
#!/bin/bash
# Check root Cargo.toml for workspace configuration
cat Cargo.toml | sed -n '/^\[workspace\]/,/^\[/p' | head -30Repository: EffortlessMetrics/shipper
Length of output: 624
🏁 Script executed:
#!/bin/bash
# Search for publish field in all Cargo.toml files
rg "^publish\s*=" crates/ --type tomlRepository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
#!/bin/bash
# Check README or docs for publication intent
if [ -f README.md ]; then
echo "=== ROOT README ==="
head -100 README.md
fi
# Check if there's any CI/docs about crate publishing
fd "README" crates/ | head -5Repository: EffortlessMetrics/shipper
Length of output: 4111
Add publish = false to internal workspace crates to prevent accidental publishing.
The workspace contains 18 crates, but the README only documents shipper and shipper-cli as public. The remaining crates (shipper-lock, shipper-process, shipper-cargo, shipper-store, shipper-types, shipper-webhook, shipper-retry, shipper-encrypt, shipper-events, shipper-auth, shipper-state, shipper-config, shipper-git, shipper-registry, shipper-environment, shipper-storage) are internal and should explicitly set publish = false in their [package] sections. This prevents accidental publishing and clarifies publication intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 104 - 133, Add publish = false to
the [package] section of each internal crate to prevent accidental publishing:
update the Cargo.toml for the crates shipper-lock, shipper-process,
shipper-cargo, shipper-store, shipper-types, shipper-webhook, shipper-retry,
shipper-encrypt, shipper-events, shipper-auth, shipper-state, shipper-config,
shipper-git, shipper-registry, shipper-environment, and shipper-storage by
adding publish = false under [package]; keep shipper and shipper-cli without
this flag so they remain publishable and re-run the workflow to confirm only the
intended crates are published.
| - name: Publish shipper crate | ||
| run: cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }} | ||
| continue-on-error: true # May already be published | ||
|
|
||
| - name: Wait for index update | ||
| run: sleep 30 |
There was a problem hiding this comment.
continue-on-error: true silently swallows real publish failures and produces a false-green release.
The comment says "May already be published," but any failure — expired token, rate limit, a crate-level validation error — is equally silenced. If shipper fails to publish but continue-on-error passes the step, the sleep 30 and shipper-cli publish run regardless. If shipper-cli depends on shipper, the second publish will fail with a resolver error, but if it doesn't, you now have a GitHub release artifact without the library crate.
At a minimum, check the exit code explicitly or use cargo search to gate the "already published" case:
🛡️ More robust publish pattern
- - name: Publish shipper crate
- run: cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- continue-on-error: true # May already be published
+ - name: Publish shipper crate
+ run: |
+ VERSION=$(cargo metadata --no-deps --format-version 1 \
+ | python3 -c "import sys,json; pkgs=json.load(sys.stdin)['packages']; \
+ print(next(p['version'] for p in pkgs if p['name']=='shipper'))")
+ if cargo search shipper --limit 1 | grep -q "shipper = \"$VERSION\""; then
+ echo "shipper $VERSION already published, skipping."
+ else
+ cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
+ fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Publish shipper crate | |
| run: cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| continue-on-error: true # May already be published | |
| - name: Wait for index update | |
| run: sleep 30 | |
| - name: Publish shipper crate | |
| run: | | |
| VERSION=$(cargo metadata --no-deps --format-version 1 \ | |
| | python3 -c "import sys,json; pkgs=json.load(sys.stdin)['packages']; \ | |
| print(next(p['version'] for p in pkgs if p['name']=='shipper'))") | |
| if cargo search shipper --limit 1 | grep -q "shipper = \"$VERSION\""; then | |
| echo "shipper $VERSION already published, skipping." | |
| else | |
| cargo publish -p shipper --token ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| fi | |
| - name: Wait for index update | |
| run: sleep 30 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 125 - 130, The workflow step
"Publish shipper crate" uses continue-on-error: true which hides real publish
failures; remove that flag and instead run cargo publish for the "Publish
shipper crate" step without continuing on error, and add an explicit gate: if
cargo publish fails, check whether the failure is only because the crate is
already published (use a cargo search/check for "shipper") and allow only that
specific case to succeed, otherwise fail the job; ensure the subsequent "Wait
for index update" / later "shipper-cli" publish steps depend on this explicit
success check so you don't proceed on masked errors.
| - name: Wait for index update | ||
| run: sleep 30 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
sleep 30 is too fragile for crates.io index propagation.
Thirty seconds is a best-guess delay. If shipper-cli declares a path or version dependency on shipper, Cargo will try to resolve it from the sparse index during cargo publish, which may not reflect the just-published version yet. A polling loop is more reliable:
♻️ More resilient wait step
- - name: Wait for index update
- run: sleep 30
+ - name: Wait for shipper to appear in index
+ run: |
+ VERSION=$(cargo metadata --no-deps --format-version 1 \
+ | python3 -c "import sys,json; pkgs=json.load(sys.stdin)['packages']; \
+ print(next(p['version'] for p in pkgs if p['name']=='shipper'))")
+ for i in $(seq 1 12); do
+ cargo search shipper --limit 1 | grep -q "shipper = \"$VERSION\"" && break
+ echo "Waiting for index... attempt $i/12"; sleep 15
+ done🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 129 - 130, The "Wait for index
update" step currently uses a fixed sleep (sleep 30) which is fragile; replace
it with a polling loop that queries crates.io for the just-published
crate/version (e.g., call the crates.io API endpoint for the crate or run cargo
search for the crate) and retries until the new version appears or a reasonable
timeout is reached (e.g., 5 minutes) with short backoff intervals; implement the
loop in the same release.yml step so the workflow proceeds as soon as the index
reflects the new version and fails after the timeout to avoid indefinite waits.
| /// Property: Registry name normalization is idempotent | ||
| #[test] | ||
| fn registry_normalization_idempotent(name in package_name_strategy()) { | ||
| let normalized1 = name.to_lowercase().replace('-', "_"); | ||
| let normalized2 = normalized1.to_lowercase().replace('-', "_"); | ||
| assert_eq!(normalized1, normalized2); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Normalization idempotence test is tautological.
After name.to_lowercase().replace('-', "_"), the result contains no uppercase letters or dashes. Applying the same transform again trivially produces the same output. This test doesn't catch real bugs. A more useful property test would verify normalization against the actual production normalize_registry_name function (if one exists), or test that the normalized form matches the crates.io canonicalization rules.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/property_tests.rs` around lines 36 - 42, The test
registry_normalization_idempotent is tautological; replace it to assert
normalization against the real normalization routine or canonical rules: call
the production normalize_registry_name (or crate's canonicalizer) on the
generated name from package_name_strategy and compare that result to a manual
expected canonical form (e.g., lowercase, '-'→'_', permitted char set and any
crates.io-specific rules) or assert properties of the output (only lowercase,
digits and underscores, no dashes, and follows start/end/length constraints).
Update the test to use normalize_registry_name (or import the canonicalization
function) and remove the current double-apply assertion so the test actually
verifies the intended normalization behavior.
| ## Workspace crates | ||
|
|
||
| - [`shipper-cli`](crates/shipper-cli/README.md) - installs the `shipper` binary and exposes the command-line workflow. | ||
| - [`shipper`](crates/shipper/README.md) - reusable library for planning, preflight checks, publish execution, resume, and receipts. |
There was a problem hiding this comment.
"Workspace crates" section omits the new micro-crates introduced in this PR.
The PR explicitly adds shipper-lock, shipper-process, shipper-cargo, and a number of other micro-backend crates intended for direct consumer use via micro-* features. Omitting them from this section leaves library consumers without guidance on which crates to add as dependencies.
If these crates are intentionally private/internal, consider adding a brief note or a publish = false entry in their manifests to make that explicit.
🧰 Tools
🪛 LanguageTool
[typographical] ~17-~17: To join two clauses or introduce examples, consider using an em dash.
Context: ...pper-cli](crates/shipper-cli/README.md) - installs the shipper` binary and expose...
(DASH_RULE)
[typographical] ~18-~18: To join two clauses or introduce examples, consider using an em dash.
Context: ... - shipper - reusable library for planning, preflight...
(DASH_RULE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 15 - 18, Update the "Workspace crates" section to
list the newly added crates (e.g., shipper-lock, shipper-process, shipper-cargo
and any other micro-backend crates exposed via micro-* features) alongside the
existing shipper-cli and shipper entries, or add a short note stating those
micro-crates are internal and set publish = false in their Cargo.toml manifests;
ensure the README clearly indicates which crates are intended for direct
consumer dependencies and which are internal/private.
| - [`shipper-cli`](crates/shipper-cli/README.md) - installs the `shipper` binary and exposes the command-line workflow. | ||
| - [`shipper`](crates/shipper/README.md) - reusable library for planning, preflight checks, publish execution, resume, and receipts. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Nitpick: use em dashes for clarity (static analysis hint).
Lines 17–18 use a hyphen-space (-) as a separator between the crate link and its description. An em dash (—) reads more clearly when joining two clauses.
✏️ Proposed fix
-- [`shipper-cli`](crates/shipper-cli/README.md) - installs the `shipper` binary and exposes the command-line workflow.
-- [`shipper`](crates/shipper/README.md) - reusable library for planning, preflight checks, publish execution, resume, and receipts.
+- [`shipper-cli`](crates/shipper-cli/README.md) — installs the `shipper` binary and exposes the command-line workflow.
+- [`shipper`](crates/shipper/README.md) — reusable library for planning, preflight checks, publish execution, resume, and receipts.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - [`shipper-cli`](crates/shipper-cli/README.md) - installs the `shipper` binary and exposes the command-line workflow. | |
| - [`shipper`](crates/shipper/README.md) - reusable library for planning, preflight checks, publish execution, resume, and receipts. | |
| - [`shipper-cli`](crates/shipper-cli/README.md) — installs the `shipper` binary and exposes the command-line workflow. | |
| - [`shipper`](crates/shipper/README.md) — reusable library for planning, preflight checks, publish execution, resume, and receipts. |
🧰 Tools
🪛 LanguageTool
[typographical] ~17-~17: To join two clauses or introduce examples, consider using an em dash.
Context: ...pper-cli](crates/shipper-cli/README.md) - installs the shipper` binary and expose...
(DASH_RULE)
[typographical] ~18-~18: To join two clauses or introduce examples, consider using an em dash.
Context: ... - shipper - reusable library for planning, preflight...
(DASH_RULE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 17 - 18, Replace the hyphen-space separator with an
em dash between the crate link and its description for the entries referencing
`shipper-cli` and `shipper` in the README; update the two lines that currently
read "[`shipper-cli`](crates/shipper-cli/README.md) - installs..." and
"[`shipper`](crates/shipper/README.md) - reusable..." to use an em dash (`—`)
instead of the hyphen so the links `shipper-cli` and `shipper` read more
clearly.
| |-----------|---------| | ||
| | **Description** | Machine-readable output for all commands | | ||
| | **User Benefit** | Parse Shipper output in scripts and tooling | | ||
| | **Technical Approach** | Add `--format json|yaml` flag to relevant commands | |
There was a problem hiding this comment.
Escape the pipe inside the code span to fix MD056 table column count warning.
The | in `--format json|yaml` is being interpreted as a table column separator by markdownlint-cli2, giving this row 3 columns against the 2-column header.
📝 Proposed fix
-| **Technical Approach** | Add `--format json|yaml` flag to relevant commands |
+| **Technical Approach** | Add `--format json\|yaml` flag to relevant commands |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | **Technical Approach** | Add `--format json|yaml` flag to relevant commands | | |
| | **Technical Approach** | Add `--format json\|yaml` flag to relevant commands | |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 466-466: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ROADMAP.md` at line 466, The markdown table row contains a code span
`--format json|yaml` whose pipe is being parsed as a column separator; fix the
MD056 warning by escaping the pipe inside the code span (e.g., change the
`--format json|yaml` token so the '|' is escaped or replaced with an entity) in
the ROADMAP.md table entry where `--format json|yaml` appears so the table
remains two columns.
| | Changelog generation | Use [release-plz](https://github.com/MarcoIeni/release-plz) | | ||
| | Git tag creation | Use cargo-release | | ||
| | GitHub release creation | Use `gh` CLI or GitHub Actions | | ||
| | crate.io team management | Use `cargo owner` directly | |
There was a problem hiding this comment.
Typo: crate.io → crates.io.
📝 Proposed fix
-| crate.io team management | Use `cargo owner` directly |
+| crates.io team management | Use `cargo owner` directly |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | crate.io team management | Use `cargo owner` directly | | |
| | crates.io team management | Use `cargo owner` directly | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ROADMAP.md` at line 520, There is a typo in the table row text "crate.io team
management" — change "crate.io" to the correct registry name "crates.io" so the
row reads "crates.io team management"; update the string in the table entry
where "crate.io team management" appears.
| [profile.ci] | ||
| # Fail fast on CI to save time | ||
| fail-fast = false |
There was a problem hiding this comment.
Misleading comment: fail-fast = false does the opposite of "fail fast."
The comment says "Fail fast on CI to save time" but the setting fail-fast = false means the test run will continue after the first failure, collecting all results. Either flip the value to true if you actually want fail-fast behavior, or fix the comment.
Proposed fix (if intent is to run all tests)
[profile.ci]
-# Fail fast on CI to save time
-fail-fast = false
+# Run all tests even if some fail, to collect full results
+fail-fast = false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [profile.ci] | |
| # Fail fast on CI to save time | |
| fail-fast = false | |
| [profile.ci] | |
| # Run all tests even if some fail, to collect full results | |
| fail-fast = false |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.config/nextest.toml around lines 4 - 6, The comment above the [profile.ci]
section is misleading: it reads "Fail fast on CI to save time" while the setting
fail-fast = false disables fail-fast. Either change the setting to fail-fast =
true to enable true fail-fast behavior in the profile.ci configuration, or
update the comment to accurately state that fail-fast is disabled (e.g., "Do not
fail fast on CI; collect all test results") so the comment matches the fail-fast
value.
| [profile.nightly] | ||
| # Nightly profile for extended testing | ||
| retries = { count = 3, backoff = "exponential", delay = "2s" } | ||
| test-threads = "num-cpus" No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline at end of file.
Add a newline after line 31 for POSIX compliance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.config/nextest.toml around lines 28 - 31, The file ends without a trailing
newline; please add a single POSIX-compliant newline character at the end of the
file (after the [profile.nightly] block that contains retries and test-threads)
so the file terminates with a newline.
| - name: Check with MSRV | ||
| run: cargo check --workspace |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
MSRV check runs without --all-features.
cargo check --workspace only validates the default feature set against MSRV 1.92. Feature-gated code (e.g., micro-* features) may use newer Rust APIs and won't be caught here. Consider adding --all-features or at minimum --features micro-all if full MSRV coverage is desired.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 114 - 115, The MSRV check step named
"Check with MSRV" currently runs cargo check --workspace which only checks
default features; update that step to run cargo check --workspace --all-features
(or at minimum cargo check --workspace --features micro-all) so feature-gated
code is validated against the MSRV; modify the workflow command where "Check
with MSRV" invokes cargo to include the chosen feature flag.
| fuzz-smoke: | ||
| name: Fuzz Smoke (PR) | ||
| if: github.event_name != 'schedule' | ||
| runs-on: ubuntu-latest |
There was a problem hiding this comment.
fuzz-smoke skips on schedule, contradicting the nightly cron intent.
The schedule trigger (line 8–10) is documented as "Nightly quick fuzz and elevated property tests", yet fuzz-smoke has if: github.event_name != 'schedule' which excludes it from scheduled runs. If nightly fuzz runs are intended, either remove this condition or add a separate scheduled fuzz job. If a separate fuzz.yml workflow handles nightly fuzzing, consider updating the cron comment to avoid confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 230 - 233, The fuzz-smoke job
currently excludes scheduled runs via the condition "if: github.event_name !=
'schedule'", which prevents it from running on the nightly cron; locate the
fuzz-smoke job block and either remove that "if" condition so the job runs for
schedule events, or add a separate job/workflow configured with on: schedule to
run fuzz-smoke at night; update the cron comment if a separate scheduled
workflow already handles nightly fuzzing to avoid confusion.
| targets: | ||
| description: 'Specific targets to run (comma-separated, or "all")' | ||
| required: false | ||
| default: 'all' |
There was a problem hiding this comment.
targets input is declared but never used.
The targets input allows callers to specify specific fuzz targets, but the matrix on lines 38–43 is static and always runs all five targets. Either wire the input to filter the matrix (e.g., via a preceding job that outputs the filtered list) or remove the unused input to avoid confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/fuzz.yml around lines 17 - 20, The workflow declares an
unused "targets" input but the job's strategy.matrix currently hardcodes the
five targets; either remove the "targets" input or wire it into the matrix. To
fix: either delete the "targets" workflow input declaration, or change the job's
strategy.matrix.targets to be driven by the declared input (convert the
comma-separated inputs.targets into an array for the matrix or produce a
filtered list via a small preceding job that outputs the JSON array) so the job
uses the input instead of the static list; look for the "targets" input
declaration and the job block using strategy.matrix.targets to update
accordingly.
| Some(status) => { | ||
| let mut stdout_bytes = Vec::new(); | ||
| let mut stderr_bytes = Vec::new(); | ||
| if let Some(mut out) = child.stdout.take() { | ||
| let _ = out.read_to_end(&mut stdout_bytes); | ||
| } | ||
| if let Some(mut err) = child.stderr.take() { | ||
| let _ = err.read_to_end(&mut stderr_bytes); | ||
| } | ||
| break ( | ||
| status.code().unwrap_or(-1), | ||
| String::from_utf8_lossy(&stdout_bytes).to_string(), | ||
| String::from_utf8_lossy(&stderr_bytes).to_string(), | ||
| false, | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Pipe-read duplication between normal-exit and timeout-exit paths.
The stdout/stderr reading pattern (lines 41–48 and 61–68) is duplicated. Consider extracting a small helper to reduce repetition.
♻️ Example helper extraction
+fn read_child_pipes(child: &mut std::process::Child) -> (String, String) {
+ let mut stdout_bytes = Vec::new();
+ let mut stderr_bytes = Vec::new();
+ if let Some(mut out) = child.stdout.take() {
+ let _ = out.read_to_end(&mut stdout_bytes);
+ }
+ if let Some(mut err) = child.stderr.take() {
+ let _ = err.read_to_end(&mut stderr_bytes);
+ }
+ (
+ String::from_utf8_lossy(&stdout_bytes).to_string(),
+ String::from_utf8_lossy(&stderr_bytes).to_string(),
+ )
+}Also applies to: 61-68
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/process.rs` around lines 40 - 54, The stdout/stderr read
logic is duplicated in the normal-exit and timeout-exit branches in
crates/shipper/src/process.rs; extract a small helper (e.g., a private function
like read_child_output(child: &mut Child) -> (String, String) or
read_and_collect_outputs(child: &mut Child) -> (Vec<u8>, Vec<u8>)) that takes
the process Child (or mutable references to its stdout/stderr), performs the
take/read_to_end steps, and returns the collected stdout/stderr (as bytes or
strings), then call that helper from both the Some(status) branch and the
timeout branch and use its results when building the returned tuple (status
code, stdout, stderr, timed_out flag).
| proptest! { | ||
| /// Property: Package state serialization roundtrips correctly | ||
| #[test] | ||
| fn package_state_roundtrip( | ||
| state in prop_oneof![ | ||
| Just(PackageState::Pending), | ||
| Just(PackageState::Uploaded), | ||
| Just(PackageState::Published), | ||
| Just(PackageState::Skipped { reason: "test".to_string() }), | ||
| Just(PackageState::Failed { class: ErrorClass::Retryable, message: "error".to_string() }), | ||
| Just(PackageState::Ambiguous { message: "maybe".to_string() }), | ||
| ] | ||
| ) { | ||
| let json = serde_json::to_string(&state).unwrap(); | ||
| let parsed: PackageState = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(state, parsed); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Proptest with only Just() values doesn't exercise property-based testing.
package_state_roundtrip and error_class_roundtrip use prop_oneof! over fixed Just(...) values — this is equivalent to a parameterized test with a known set of inputs. Consider generating arbitrary String fields (reason, message) via proptest strategies so the roundtrip property is tested across random payloads, which would actually catch edge cases like special characters in JSON serialization.
♻️ Suggested improvement for package_state_roundtrip
#[test]
fn package_state_roundtrip(
- state in prop_oneof![
- Just(PackageState::Pending),
- Just(PackageState::Uploaded),
- Just(PackageState::Published),
- Just(PackageState::Skipped { reason: "test".to_string() }),
- Just(PackageState::Failed { class: ErrorClass::Retryable, message: "error".to_string() }),
- Just(PackageState::Ambiguous { message: "maybe".to_string() }),
- ]
+ state in prop_oneof![
+ Just(PackageState::Pending),
+ Just(PackageState::Uploaded),
+ Just(PackageState::Published),
+ ".*".prop_map(|s| PackageState::Skipped { reason: s }),
+ (".*", prop_oneof![Just(ErrorClass::Retryable), Just(ErrorClass::Permanent), Just(ErrorClass::Ambiguous)])
+ .prop_map(|(msg, class)| PackageState::Failed { class, message: msg }),
+ ".*".prop_map(|s| PackageState::Ambiguous { message: s }),
+ ]
) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| proptest! { | |
| /// Property: Package state serialization roundtrips correctly | |
| #[test] | |
| fn package_state_roundtrip( | |
| state in prop_oneof![ | |
| Just(PackageState::Pending), | |
| Just(PackageState::Uploaded), | |
| Just(PackageState::Published), | |
| Just(PackageState::Skipped { reason: "test".to_string() }), | |
| Just(PackageState::Failed { class: ErrorClass::Retryable, message: "error".to_string() }), | |
| Just(PackageState::Ambiguous { message: "maybe".to_string() }), | |
| ] | |
| ) { | |
| let json = serde_json::to_string(&state).unwrap(); | |
| let parsed: PackageState = serde_json::from_str(&json).unwrap(); | |
| assert_eq!(state, parsed); | |
| } | |
| proptest! { | |
| /// Property: Package state serialization roundtrips correctly | |
| #[test] | |
| fn package_state_roundtrip( | |
| state in prop_oneof![ | |
| Just(PackageState::Pending), | |
| Just(PackageState::Uploaded), | |
| Just(PackageState::Published), | |
| ".*".prop_map(|s| PackageState::Skipped { reason: s }), | |
| (".*", prop_oneof![Just(ErrorClass::Retryable), Just(ErrorClass::Permanent), Just(ErrorClass::Ambiguous)]) | |
| .prop_map(|(msg, class)| PackageState::Failed { class, message: msg }), | |
| ".*".prop_map(|s| PackageState::Ambiguous { message: s }), | |
| ] | |
| ) { | |
| let json = serde_json::to_string(&state).unwrap(); | |
| let parsed: PackageState = serde_json::from_str(&json).unwrap(); | |
| assert_eq!(state, parsed); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/property_tests.rs` around lines 18 - 34, The tests
package_state_roundtrip (and similarly error_class_roundtrip) currently use only
fixed Just(...) values so they don't exercise random inputs; replace the fixed
Just(...) for variants that carry text with proptest string strategies (e.g.,
generate reason/message using any::<String>() or
proptest::string::string_regex(...)?) and build the PackageState variants from
those strategies so proptest will produce many different strings (including edge
cases) — update the proptest! invocation in package_state_roundtrip to use a
prop_oneof! that includes strategies like (reason in
any::<String>()).prop_map(|reason| PackageState::Skipped { reason }) and (class
in any::<ErrorClass>(), message in any::<String>()).prop_map(|(class,message)|
PackageState::Failed { class, message }) (and similarly for Ambiguous), and
apply the same approach in error_class_roundtrip to ensure the roundtrip
property is validated over random payloads.
| ### Token Sources (in priority order) | ||
|
|
||
| 1. `CARGO_REGISTRY_TOKEN` environment variable | ||
| 2. `CARGO_TOKEN` environment variable (fallback) | ||
| 3. Cargo credential store (`cargo login` configuration) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, explore the repository structure to find the auth-related code
echo "=== Repository structure ==="
git ls-files | grep -E "(auth|token|credential)" | head -20
echo ""
echo "=== Looking for shipper-auth crate or similar ==="
find . -name "Cargo.toml" -type f | head -10Repository: EffortlessMetrics/shipper
Length of output: 628
🏁 Script executed:
#!/bin/bash
# Examine the main auth implementation to find token resolution order
echo "=== Token resolution in shipper-auth lib ==="
head -150 crates/shipper-auth/src/lib.rs
echo ""
echo "=== Check auth.rs ==="
head -100 crates/shipper/src/auth.rs
echo ""
echo "=== Check resolve_token fuzzing target (likely shows priority) ==="
cat fuzz/fuzz_targets/resolve_token.rsRepository: EffortlessMetrics/shipper
Length of output: 8676
🏁 Script executed:
#!/bin/bash
# Verify the SECURITY.md content to confirm the discrepancy
echo "=== SECURITY.md lines 60-64 ==="
sed -n '60,64p' SECURITY.md
echo ""
echo "=== Search for CARGO_TOKEN in the codebase to see if it's actually used ==="
rg "CARGO_TOKEN" --type=rust -iRepository: EffortlessMetrics/shipper
Length of output: 692
🏁 Script executed:
#!/bin/bash
# Check if CARGO_TOKEN is actually used anywhere in token resolution
echo "=== Check cargo.rs for context on find_cargo_token_env ==="
grep -B5 -A10 "find_cargo_token_env" crates/shipper/src/cargo.rs | head -40
echo ""
echo "=== Verify SECURITY.md doesn't mention CARGO_REGISTRIES_<NAME>_TOKEN ==="
grep -n "CARGO_REGISTRIES" SECURITY.mdRepository: EffortlessMetrics/shipper
Length of output: 1041
🏁 Script executed:
#!/bin/bash
# Double-check if CARGO_TOKEN (exactly, not CARGO_REGISTRY_TOKEN or CARGO_REGISTRIES) is mentioned anywhere in token resolution
echo "=== Search for actual 'CARGO_TOKEN' (not variants) in implementation ==="
rg "\"CARGO_TOKEN\"" --type=rust
rg "'CARGO_TOKEN'" --type=rust
echo ""
echo "=== Comprehensive check: all env var references in shipper-auth ==="
rg "env::var" crates/shipper-auth/src/lib.rsRepository: EffortlessMetrics/shipper
Length of output: 363
SECURITY.md incorrectly documents token resolution order.
The document lists CARGO_TOKEN as the second priority, but the actual implementation (shipper-auth crate) uses CARGO_REGISTRIES_<NAME>_TOKEN instead. The correct precedence is:
CARGO_REGISTRY_TOKENenvironment variableCARGO_REGISTRIES_<NAME>_TOKENenvironment variable$CARGO_HOME/credentials.tomlfile
Update SECURITY.md lines 60-64 to reflect the actual implementation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@SECURITY.md` around lines 60 - 64, SECURITY.md's token precedence is
incorrect; update the "Token Sources" list to match the shipper-auth
implementation by replacing the second item with the environment variable form
used by the crate: list priority as 1) CARGO_REGISTRY_TOKEN env var, 2)
CARGO_REGISTRIES_<NAME>_TOKEN env var (the per-registry token format
shipper-auth reads), and 3) the $CARGO_HOME/credentials.toml file (Cargo
credential store); ensure the text references these exact symbols
(CARGO_REGISTRY_TOKEN, CARGO_REGISTRIES_<NAME>_TOKEN, credentials.toml) so it
matches the implementation.
| ### File Permissions | ||
|
|
||
| Shipper creates files with appropriate permissions: | ||
|
|
||
| - State files: User read/write only (0600 on Unix) | ||
| - Lock files: User read/write only (0600 on Unix) | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if file permissions are explicitly set in the codebase
echo "=== Permission setting code ==="
rg -n '0o600|permissions|set_permissions|mode\(0o' --type=rust -C3
echo ""
echo "=== File creation patterns ==="
rg -n 'File::create|OpenOptions|fs::write' --type=rust -C3
echo ""
echo "=== Search for lock file creation ==="
fd . -type f -name '*lock*' | head -20
echo ""
echo "=== Search for state file creation ==="
fd . -type f -name '*state*' | head -20Repository: EffortlessMetrics/shipper
Length of output: 50742
🏁 Script executed:
#!/bin/bash
# Search specifically for 0o600
echo "=== Searching for 0o600 specifically ==="
rg '0o600' --type=rust
# Check state file creation implementation
echo -e "\n=== State file creation in state.rs ==="
rg -A20 'fn save_state' crates/shipper-state/src/lib.rs
# Check lock file creation implementation
echo -e "\n=== Lock file creation in lock.rs ==="
rg -A25 'fn acquire' crates/shipper-lock/src/lib.rs | head -60
# Check storage implementation
echo -e "\n=== Storage write implementation ==="
rg -B5 -A15 'fn write_file' crates/shipper-storage/src/lib.rsRepository: EffortlessMetrics/shipper
Length of output: 5065
🏁 Script executed:
#!/bin/bash
echo "=== Checking atomic_write_json implementation ==="
rg -A30 'fn atomic_write_json' crates/shipper-state/src/lib.rs
echo -e "\n=== Checking for any Unix permission handling ==="
rg -B2 -A2 'PermissionsExt|set_permissions|chmod' crates/shipper-*/src/*.rs crates/shipper/src/*.rsRepository: EffortlessMetrics/shipper
Length of output: 7771
Verify and fix file permissions enforcement for state and lock files.
The code does not enforce 0o600 permissions as claimed in SECURITY.md. State and lock files are created using fs::File::create() without explicit permission setting via PermissionsExt::set_mode(), resulting in default permissions (typically 0o644) instead of the documented user read/write-only (0o600). This creates a security documentation-implementation mismatch where the security claim is misleading.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@SECURITY.md` around lines 89 - 95, The code creates state/lock files with
fs::File::create() but does not enforce the documented 0o600 mode; update the
file-creation sites (e.g., create_state_file and create_lock_file or any
locations calling fs::File::create()) to set strict permissions: either open the
file with std::fs::OpenOptions and
std::os::unix::fs::OpenOptionsExt::mode(0o600) or call
file.set_permissions(std::os::unix::fs::PermissionsExt::from_mode(0o600))
immediately after creation; ensure both state and lock file paths use this
change so the files are created with user read/write only as claimed in
SECURITY.md.
| Shipper itself is published to crates.io. To verify authenticity: | ||
|
|
||
| 1. **Verify the source**: | ||
| ```bash | ||
| cargo download shipper-cli | ||
| # Compare with source at github.com/cmrigney/shipper | ||
| ``` | ||
|
|
||
| 2. **Verify the checksum**: | ||
| ```bash | ||
| sha256sum ~/.cargo/registry/cache/*/shipper-*.crate | ||
| ``` | ||
|
|
||
| 3. **Build from source** for maximum trust: | ||
| ```bash | ||
| git clone https://github.com/cmrigney/shipper.git | ||
| cd shipper | ||
| cargo build --release -p shipper-cli | ||
| ``` |
There was a problem hiding this comment.
Fix markdownlint warnings: blank lines around fenced code blocks and missing trailing newline.
Static analysis flagged:
- Lines 103, 109, 114: Fenced code blocks inside numbered list items need surrounding blank lines (MD031).
- Line 163: File should end with a single newline character (MD047).
Also applies to: 163-163
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 103-103: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 109-109: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 114-114: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@SECURITY.md` around lines 100 - 118, Add blank lines before and after each
fenced code block inside the numbered list items in the SECURITY.md snippet (the
three blocks under "Verify the source", "Verify the checksum", and "Build from
source") so they conform to MD031, and ensure the file ends with a single
trailing newline (MD047); update the three blocks that start with ```bash and
the final newline at EOF accordingly.
| let (exit_code, stdout, stderr, timed_out) = if let Some(timeout_dur) = timeout { | ||
| let mut child = cmd | ||
| .stdout(std::process::Stdio::piped()) | ||
| .stderr(std::process::Stdio::piped()) | ||
| .spawn() | ||
| .context("failed to execute cargo publish; is Cargo installed?")?; | ||
|
|
||
| let deadline = Instant::now() + timeout_dur; | ||
| loop { | ||
| match child.try_wait().context("failed to poll cargo process")? { | ||
| Some(status) => { | ||
| let mut stdout_bytes = Vec::new(); | ||
| let mut stderr_bytes = Vec::new(); | ||
| if let Some(mut out) = child.stdout.take() { | ||
| let _ = out.read_to_end(&mut stdout_bytes); | ||
| } | ||
| if let Some(mut err) = child.stderr.take() { | ||
| let _ = err.read_to_end(&mut stderr_bytes); | ||
| } | ||
| break ( | ||
| status.code().unwrap_or(-1), | ||
| String::from_utf8_lossy(&stdout_bytes).to_string(), | ||
| String::from_utf8_lossy(&stderr_bytes).to_string(), | ||
| false, | ||
| ); | ||
| } | ||
| None => { | ||
| if Instant::now() >= deadline { | ||
| let _ = child.kill(); | ||
| let _ = child.wait(); | ||
| let mut stdout_bytes = Vec::new(); | ||
| let mut stderr_bytes = Vec::new(); | ||
| if let Some(mut out) = child.stdout.take() { | ||
| let _ = out.read_to_end(&mut stdout_bytes); | ||
| } | ||
| if let Some(mut err) = child.stderr.take() { | ||
| let _ = err.read_to_end(&mut stderr_bytes); | ||
| } | ||
| let mut stderr_str = String::from_utf8_lossy(&stderr_bytes).to_string(); | ||
| stderr_str.push_str(&format!( | ||
| "\ncargo publish timed out after {}", | ||
| humantime::format_duration(timeout_dur) | ||
| )); | ||
| break ( | ||
| -1, | ||
| String::from_utf8_lossy(&stdout_bytes).to_string(), | ||
| stderr_str, | ||
| true, | ||
| ); | ||
| } | ||
| std::thread::sleep(Duration::from_millis(100)); |
There was a problem hiding this comment.
Timeout path can deadlock on stdout/stderr backpressure.
When the child is running, stdout/stderr are piped but never drained, so a chatty cargo publish can block once buffers fill, leading to a false timeout. Consider draining pipes in helper threads or reusing the process module’s timeout logic that streams output.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cargo/src/lib.rs` around lines 79 - 129, The timeout branch
deadlocks because stdout/stderr are only read after the child exits; fix by
capturing child.stdout and child.stderr immediately after spawn and spawn
background reader threads that continuously read_to_end into shared buffers (or
channels) so backpressure is drained while the loop with
child.try_wait()/deadline runs; ensure the reader results are joined/collected
into the stdout_bytes/stderr_bytes used in both the Some(status) and timeout
branches so symbols like child, child.stdout/child.stderr, stdout_bytes,
stderr_bytes, try_wait, deadline, and timeout_dur are preserved and populated
regardless of whether the process exits or is killed.
| /// Get packages in topological order (dependencies first) | ||
| pub fn topological_order(&self) -> Result<Vec<String>> { | ||
| let mut order = Vec::new(); | ||
| let mut visited = HashSet::new(); | ||
| let mut visiting = HashSet::new(); | ||
|
|
||
| // Build dependency graph | ||
| let dep_graph = self.build_dependency_graph(); | ||
|
|
||
| for package in self.publishable_packages() { | ||
| let name = package.name.to_string(); | ||
| self.visit_package(&name, &dep_graph, &mut visited, &mut visiting, &mut order)?; | ||
| } | ||
|
|
||
| Ok(order) | ||
| } | ||
|
|
||
| fn visit_package( | ||
| &self, | ||
| name: &str, | ||
| dep_graph: &HashMap<String, Vec<String>>, | ||
| visited: &mut HashSet<String>, | ||
| visiting: &mut HashSet<String>, | ||
| order: &mut Vec<String>, | ||
| ) -> Result<()> { | ||
| if visited.contains(name) { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| if visiting.contains(name) { | ||
| return Err(anyhow::anyhow!( | ||
| "circular dependency detected involving {}", | ||
| name | ||
| )); | ||
| } | ||
|
|
||
| visiting.insert(name.to_string()); | ||
|
|
||
| if let Some(deps) = dep_graph.get(name) { | ||
| for dep in deps { | ||
| self.visit_package(dep, dep_graph, visited, visiting, order)?; | ||
| } | ||
| } | ||
|
|
||
| visiting.remove(name); | ||
| visited.insert(name.to_string()); | ||
| order.push(name.to_string()); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn build_dependency_graph(&self) -> HashMap<String, Vec<String>> { | ||
| let mut graph = HashMap::new(); | ||
|
|
||
| for package in self.publishable_packages() { | ||
| let deps: Vec<String> = package | ||
| .dependencies | ||
| .iter() | ||
| .filter_map(|dep| { | ||
| // Only include workspace dependencies | ||
| self.metadata | ||
| .packages | ||
| .iter() | ||
| .find(|p| p.name == dep.name) | ||
| .map(|p| p.name.to_string()) | ||
| }) | ||
| .collect(); | ||
|
|
||
| graph.insert(package.name.to_string(), deps); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all usages of topological_order
rg -n "topological_order\(" crates -g '*.rs'Repository: EffortlessMetrics/shipper
Length of output: 336
🏁 Script executed:
#!/bin/bash
# Check the file to understand the broader context
wc -l crates/shipper-cargo/src/lib.rsRepository: EffortlessMetrics/shipper
Length of output: 104
🏁 Script executed:
#!/bin/bash
# Look at the full context around the function to understand how it's used
sed -n '416,500p' crates/shipper-cargo/src/lib.rsRepository: EffortlessMetrics/shipper
Length of output: 2510
🏁 Script executed:
#!/bin/bash
# Check if there are tests for topological_order
rg -n "topological_order" crates -g '*.rs' -A 5 -B 2 | head -100Repository: EffortlessMetrics/shipper
Length of output: 1561
🏁 Script executed:
#!/bin/bash
# Find the publishable_packages method definition
rg -n "fn publishable_packages" crates -g '*.rs' -A 10Repository: EffortlessMetrics/shipper
Length of output: 816
🏁 Script executed:
#!/bin/bash
# Check if topological_order is used in actual publish logic
rg -n "topological_order" crates -g '*.rs' -B 5 -A 5Repository: EffortlessMetrics/shipper
Length of output: 1931
🏁 Script executed:
#!/bin/bash
# Look for any references to publish sequencing or ordering
rg -n "publish|sequence" crates/shipper-cargo/src/lib.rs -i | head -30Repository: EffortlessMetrics/shipper
Length of output: 1689
🏁 Script executed:
#!/bin/bash
# Check for any CLI references to topological order
rg -n "topological" crates/shipper-cli -g '*.rs'Repository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
#!/bin/bash
# Look for TODO/FIXME comments around topological_order
rg -n "topological|TODO|FIXME" crates/shipper-cargo/src/lib.rs -B 2 -A 2Repository: EffortlessMetrics/shipper
Length of output: 825
🏁 Script executed:
#!/bin/bash
# Check if there's any reference in main.rs or CLI entrypoints
find crates -name "main.rs" -o -name "cli.rs" | xargs grep -l "topological" 2>/dev/null || echo "No matches"Repository: EffortlessMetrics/shipper
Length of output: 79
Use deterministic data structures for topological ordering.
HashMap and HashSet have nondeterministic iteration order. While the current implementation is effectively deterministic (iteration occurs over Vec types), replace them with BTreeMap/BTreeSet or use Kahn's algorithm with deterministic queues to make the deterministic guarantees explicit and improve code clarity. Additionally, the test workspace_metadata_topological_order() should verify the ordering behavior rather than just checking result.is_ok() || result.is_err().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cargo/src/lib.rs` around lines 416 - 485, Replace
non-deterministic collections with deterministic ones: change
build_dependency_graph to return a BTreeMap<String, Vec<String>> (or BTreeSet
where appropriate) instead of HashMap, and in topological_order and
visit_package use BTreeSet<String> for visited/visiting (or otherwise use
BTreeMap lookups) so iteration order is stable; ensure you iterate
publishable_packages() in a sorted/deterministic order (e.g., sort by
package.name) before visiting. Update signatures: build_dependency_graph(&self)
-> BTreeMap<String, Vec<String>> and visit_package to accept &BTreeMap; convert
all HashSet/HashMap usages (visited, visiting, dep_graph) accordingly. Also
update the test workspace_metadata_topological_order() to assert a concrete
expected ordering or properties of the ordering instead of only checking
result.is_ok()/is_err().
| /// Enable encryption for state files | ||
| #[arg(long, global = true)] | ||
| encrypt: bool, | ||
|
|
||
| /// Passphrase for state file encryption (or use SHIPPER_ENCRYPT_KEY env var) | ||
| #[arg(long, global = true)] | ||
| encrypt_passphrase: Option<String>, |
There was a problem hiding this comment.
Passphrase on CLI is visible in process listings.
--encrypt-passphrase exposes the secret via ps aux / /proc/*/cmdline. The help text mentions SHIPPER_ENCRYPT_KEY env var as an alternative, but consider removing the CLI flag entirely and only supporting the env var, or at minimum documenting the risk prominently. As per coding guidelines, "treat tokens as opaque strings, and never log tokens."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 151 - 157, Remove the
--encrypt-passphrase CLI option to avoid exposing secrets in process listings:
delete the encrypt_passphrase field/#[arg(...)] and any uses of that CLI flag,
and instead read the passphrase from the SHIPPER_ENCRYPT_KEY environment
variable at runtime (e.g., where you previously used encrypt_passphrase) and
treat it as opaque (do not log or print it). Update help text or comments to
mention only SHIPPER_ENCRYPT_KEY, ensure any error messages avoid echoing the
secret, and adjust any related logic that referenced encrypt_passphrase to
handle Option<String> from the env lookup instead.
| fn run_clean( | ||
| state_dir: &PathBuf, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
&PathBuf parameter should be &Path.
Clippy (clippy::ptr_arg) flags &PathBuf as less flexible than &Path. Since PathBuf auto-derefs to Path, callers can still pass &PathBuf.
Proposed fix
-fn run_clean(
- state_dir: &PathBuf,
+fn run_clean(
+ state_dir: &Path,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn run_clean( | |
| state_dir: &PathBuf, | |
| fn run_clean( | |
| state_dir: &Path, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 1276 - 1277, The function
signature for run_clean currently takes a less-flexible &PathBuf; change its
parameter type to &Path so callers can pass &Path or &PathBuf and silence
clippy::ptr_arg. Update the signature fn run_clean(state_dir: &Path) and adjust
any uses inside run_clean that call methods on PathBuf (they should work on
&Path) as well as any call sites passing &PathBuf (no change needed); ensure you
import std::path::Path if not already in scope.
| use atty::Stream; | ||
| use indicatif::{ProgressBar, ProgressStyle}; | ||
|
|
||
| /// Detects whether stdout is connected to a TTY. | ||
| pub fn is_tty() -> bool { | ||
| atty::is(Stream::Stdout) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
fd -t f "Cargo.toml" | head -5Repository: EffortlessMetrics/shipper
Length of output: 198
🏁 Script executed:
cat Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 898
🏁 Script executed:
rg -l "atty" --type rsRepository: EffortlessMetrics/shipper
Length of output: 95
🌐 Web query:
atty crate deprecated std::io::IsTerminal Rust 1.70 stabilization
💡 Result:
- The
attycrate is effectively deprecated/unmaintained: the maintainer has stated it’s no longer under development and recommends using the standard library’s terminal-detection instead. [1] - Rust 1.70.0 (released June 1, 2023) stabilized
std::io::IsTerminalwith the methodis_terminal()as the standard replacement for what crates likeattyprovided. [2][3]
Example (Rust ≥ 1.70):
use std::io::{stdout, IsTerminal};
fn main() {
if stdout().is_terminal() {
// interactive/TTY behavior (e.g., colors)
}
}- If you must support Rust older than 1.70, use the
is-terminalcrate; its docs explicitly say most users should preferstd::io::IsTerminalas of Rust 1.70. [4]
Sources: [1] RustSec advisory on atty unmaintained, [2] Rust 1.70.0 announcement, [3] std::io::IsTerminal docs, [4] is-terminal crate README.
🏁 Script executed:
rg "atty" --type-listRepository: EffortlessMetrics/shipper
Length of output: 458
🏁 Script executed:
rg "atty"Repository: EffortlessMetrics/shipper
Length of output: 455
🏁 Script executed:
cat crates/shipper-cli/Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 1790
Replace deprecated atty crate with std::io::IsTerminal.
The atty crate is deprecated and unmaintained. Since the project's MSRV is 1.92, use std::io::IsTerminal (stabilized in Rust 1.70) instead.
♻️ Proposed fix
-use atty::Stream;
+use std::io::IsTerminal;
use indicatif::{ProgressBar, ProgressStyle};
/// Detects whether stdout is connected to a TTY.
pub fn is_tty() -> bool {
- atty::is(Stream::Stdout)
+ std::io::stdout().is_terminal()
}Then remove atty = "0.2" from crates/shipper-cli/Cargo.toml.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| use atty::Stream; | |
| use indicatif::{ProgressBar, ProgressStyle}; | |
| /// Detects whether stdout is connected to a TTY. | |
| pub fn is_tty() -> bool { | |
| atty::is(Stream::Stdout) | |
| } | |
| use std::io::IsTerminal; | |
| use indicatif::{ProgressBar, ProgressStyle}; | |
| /// Detects whether stdout is connected to a TTY. | |
| pub fn is_tty() -> bool { | |
| std::io::stdout().is_terminal() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/progress.rs` around lines 8 - 14, The is_tty function
currently uses the deprecated atty crate; replace it with the standard
IsTerminal API: remove the atty::Stream import, add use std::io::IsTerminal (or
call stdout().is_terminal() directly), and implement pub fn is_tty() -> bool {
std::io::stdout().is_terminal() } (keep ProgressBar/ProgressStyle import for
indicatif). Also remove atty = "0.2" from Cargo.toml.
| // Send webhook notification: package failed | ||
| webhook::maybe_send_event( | ||
| &opts.webhook, | ||
| WebhookEvent::PublishFailed { | ||
| plan_id: ws.plan.plan_id.clone(), | ||
| package_name: p.name.clone(), | ||
| package_version: p.version.clone(), | ||
| error_class: format!("{:?}", class), | ||
| message: msg.clone(), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Webhook calls are synchronous and could slow down the publish pipeline.
webhook::maybe_send_event is called inline at multiple points during the publish flow. If the webhook endpoint is slow, unreachable, or times out, each call could delay the overall publish operation. Consider ensuring maybe_send_event uses a short timeout and/or fires asynchronously (e.g., via a background thread or channel) so the critical publish path isn't blocked.
Also applies to: 375-384, 430-438, 460-469, 518-528, 536-545
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/engine_parallel.rs` around lines 293 - 303, The inline
synchronous calls to webhook::maybe_send_event (e.g., where
WebhookEvent::PublishFailed is constructed) block the publish pipeline; update
the calls to fire asynchronously with a short timeout so slow/unreachable
endpoints don't delay publishing — either (A) change maybe_send_event to perform
its HTTP work with a short timeout and spawn it onto the background
(tokio::spawn or std::thread::spawn) and return immediately, or (B) wrap each
call site (the places that construct WebhookEvent such as the PublishFailed
invocation and the other occurrences you noted) in a background task/channel and
attach a bounded timeout; ensure failures are still logged but do not propagate
to block the publish flow.
| // Calculate publish completion statistics | ||
| let total_packages = all_receipts.len(); | ||
| let success_count = all_receipts | ||
| .iter() | ||
| .filter(|r| matches!(r.state, PackageState::Published)) | ||
| .count(); | ||
| let failure_count = all_receipts | ||
| .iter() | ||
| .filter(|r| matches!(r.state, PackageState::Failed { .. })) | ||
| .count(); | ||
| let skipped_count = all_receipts | ||
| .iter() | ||
| .filter(|r| matches!(r.state, PackageState::Skipped { .. })) | ||
| .count(); | ||
|
|
||
| let exec_result = if all_receipts.iter().all(|r| { | ||
| matches!( | ||
| r.state, | ||
| PackageState::Published | PackageState::Uploaded | PackageState::Skipped { .. } | ||
| ) | ||
| }) { | ||
| ExecutionResult::Success | ||
| } else if success_count == 0 { | ||
| ExecutionResult::CompleteFailure | ||
| } else { | ||
| ExecutionResult::PartialFailure | ||
| }; | ||
|
|
||
| // Send webhook notification: all complete | ||
| webhook::maybe_send_event( | ||
| &opts.webhook, | ||
| WebhookEvent::PublishCompleted { | ||
| plan_id: ws.plan.plan_id.clone(), | ||
| total_packages, | ||
| success_count, | ||
| failure_count, | ||
| skipped_count, | ||
| result: match exec_result { | ||
| ExecutionResult::Success => "success".to_string(), | ||
| ExecutionResult::PartialFailure => "partial_failure".to_string(), | ||
| ExecutionResult::CompleteFailure => "complete_failure".to_string(), | ||
| }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
failure_count / CompleteFailure / PartialFailure branches are unreachable.
run_publish_level (line 647) bails on any package error, so execution never reaches this point with failed packages in all_receipts. The failure_count, CompleteFailure, and PartialFailure branches are dead code. While harmless now, this could mask bugs if the error-handling in run_publish_level changes.
Consider either removing the dead branches or adding a comment noting they are defensive/future-proofing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/engine_parallel.rs` around lines 752 - 795, The statistics
and result-branching around failure_count and the ExecutionResult variants
CompleteFailure/PartialFailure are currently unreachable because
run_publish_level aborts on any package error, so all_receipts will never
contain PackageState::Failed; update the code to either remove the unreachable
failure_count calculation and the CompleteFailure/PartialFailure arms or
explicitly mark them as defensive/future-proof with a clear comment; locate
references to all_receipts, failure_count, ExecutionResult::CompleteFailure,
ExecutionResult::PartialFailure, run_publish_level, and the
webhook::maybe_send_event / WebhookEvent::PublishCompleted payload and then
either delete the dead branches and simplify the result to only Success (and
skippable Uploaded/Skipped states) or add a comment above the calculations
explaining these branches are intentionally retained for future changes in
run_publish_level error behavior.
| pub fn version_exists(&self, crate_name: &str, version: &str) -> Result<bool> { | ||
| self.registry_client | ||
| .version_exists(crate_name, version) | ||
| .or_else(|err| bail!("unexpected status while checking version existence: {err}")) | ||
| } | ||
|
|
||
| pub fn crate_exists(&self, crate_name: &str) -> Result<bool> { | ||
| self.registry_client | ||
| .crate_exists(crate_name) | ||
| .or_else(|err| bail!("unexpected status while checking crate existence: {err}")) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
or_else(|err| bail!(...)) loses the anyhow error chain.
Using bail! re-wraps the error as a new root cause, discarding the causal chain from the inner client. Prefer .context(...) or .with_context(...) to preserve it:
Proposed fix
pub fn version_exists(&self, crate_name: &str, version: &str) -> Result<bool> {
self.registry_client
.version_exists(crate_name, version)
- .or_else(|err| bail!("unexpected status while checking version existence: {err}"))
+ .context("unexpected status while checking version existence")
}
pub fn crate_exists(&self, crate_name: &str) -> Result<bool> {
self.registry_client
.crate_exists(crate_name)
- .or_else(|err| bail!("unexpected status while checking crate existence: {err}"))
+ .context("unexpected status while checking crate existence")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub fn version_exists(&self, crate_name: &str, version: &str) -> Result<bool> { | |
| self.registry_client | |
| .version_exists(crate_name, version) | |
| .or_else(|err| bail!("unexpected status while checking version existence: {err}")) | |
| } | |
| pub fn crate_exists(&self, crate_name: &str) -> Result<bool> { | |
| self.registry_client | |
| .crate_exists(crate_name) | |
| .or_else(|err| bail!("unexpected status while checking crate existence: {err}")) | |
| } | |
| pub fn version_exists(&self, crate_name: &str, version: &str) -> Result<bool> { | |
| self.registry_client | |
| .version_exists(crate_name, version) | |
| .context("unexpected status while checking version existence") | |
| } | |
| pub fn crate_exists(&self, crate_name: &str) -> Result<bool> { | |
| self.registry_client | |
| .crate_exists(crate_name) | |
| .context("unexpected status while checking crate existence") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/registry_micro.rs` around lines 28 - 38, The current
implementations of version_exists and crate_exists use .or_else(|err|
bail!(...)) which replaces the original error chain; change those calls to use
anyhow's .with_context(...) or .context(...) on the registry_client results
(e.g., registry_client.version_exists(...).with_context(|| format!("unexpected
status while checking version existence: {crate_name} {version}")) ) so the
original causal chain is preserved; also add use of anyhow::Context at the top
if not already imported and apply the same change to crate_exists (including a
descriptive message referencing crate_name) to retain error context instead of
re-bailing.
| pub fn verify_ownership(&self, crate_name: &str, token: &str) -> Result<bool> { | ||
| match self.list_owners(crate_name, token) { | ||
| Ok(_) => Ok(true), | ||
| Err(e) => { | ||
| let msg = e.to_string(); | ||
| if msg.contains("forbidden") | ||
| || msg.contains("403") | ||
| || msg.contains("unauthorized") | ||
| || msg.contains("401") | ||
| || msg.contains("not found") | ||
| || msg.contains("404") | ||
| { | ||
| Ok(false) | ||
| } else { | ||
| Err(e) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's locate the shipper_registry crate and examine its error types
find . -name "shipper_registry" -o -name "shipper-registry" -type d | head -20Repository: EffortlessMetrics/shipper
Length of output: 94
🏁 Script executed:
#!/bin/bash
# Look for error types in the codebase
rg -n "pub enum.*Error|pub struct.*Error" --type rust | grep -i "shipper\|registry" | head -20Repository: EffortlessMetrics/shipper
Length of output: 362
🏁 Script executed:
#!/bin/bash
# Examine the registry_micro.rs file to understand the context
cat -n crates/shipper/src/registry_micro.rs | head -120Repository: EffortlessMetrics/shipper
Length of output: 5211
🏁 Script executed:
#!/bin/bash
# Check what shipper_registry exposes in its public API
fd -e rs . | xargs rg -l "shipper.registry|shipper_registry" | head -10Repository: EffortlessMetrics/shipper
Length of output: 140
🏁 Script executed:
#!/bin/bash
# Examine the shipper_registry crate's main API
cat -n crates/shipper-registry/src/lib.rs | head -200Repository: EffortlessMetrics/shipper
Length of output: 8140
🏁 Script executed:
#!/bin/bash
# Search for error types and status code handling in shipper_registry
rg -n "pub enum|pub struct|StatusCode|status" crates/shipper-registry/src/lib.rs | head -30Repository: EffortlessMetrics/shipper
Length of output: 1551
🏁 Script executed:
#!/bin/bash
# Look for how errors are constructed/returned in shipper_registry
rg -n "fn list_owners|fn.*Error|impl.*Error" crates/shipper-registry/src/ -A 5 | head -50Repository: EffortlessMetrics/shipper
Length of output: 588
String matching for HTTP error conditions is incomplete and won't catch expected error cases.
The verify_ownership method searches for literal substrings "403", "401", and "404" in error messages, but shipper_registry does not include these in its error strings. The underlying fetch_owners_with_token converts HTTP status codes into descriptive messages and returns only Result<T> with anyhow errors—it does not expose status codes or structured error types.
The error messages actually returned by shipper_registry are:
- For 403/401:
"forbidden when querying owners; token may be invalid or missing required scope" - For 404:
"crate not found when querying owners: {name}" - For other status codes:
"unexpected status while querying owners: {status}"
This means lines 88, 90, and 92 (searching for "403", "401", "404") will never match. Simplify the checks to only include substrings that actually exist in the error messages: "forbidden" and "not found".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/registry_micro.rs` around lines 82 - 100, The
verify_ownership function is using literal numeric HTTP codes in its error
substring checks which never appear; update verify_ownership (and the match arm
that inspects the error message from list_owners) to only check for the actual
substrings emitted by shipper_registry—e.g., "forbidden" and "not found"—rather
than "401"/"403"/"404"; keep the existing behavior of returning Ok(false) for
those matches and Err(e) otherwise, referencing verify_ownership and list_owners
(and the underlying fetch_owners_with_token behavior) to locate where to change
the substring checks.
| loop { | ||
| attempt += 1; | ||
|
|
||
| let jittered_delay = if attempt == 1 { | ||
| Duration::ZERO | ||
| } else { | ||
| let base_delay = config.poll_interval; | ||
| let exponential_delay = base_delay | ||
| .saturating_mul(2_u32.saturating_pow(attempt.saturating_sub(2).min(16))); | ||
| let capped_delay = exponential_delay.min(config.max_delay); | ||
| let jitter_range = config.jitter_factor; | ||
| let jitter = 1.0 + (rand::random::<f64>() * 2.0 * jitter_range - jitter_range); | ||
| Duration::from_millis((capped_delay.as_millis() as f64 * jitter).round() as u64) | ||
| }; | ||
|
|
||
| let visible = match config.method { | ||
| ReadinessMethod::Api => self.version_exists(crate_name, version).unwrap_or(false), | ||
| ReadinessMethod::Index => self | ||
| .check_index_visibility(crate_name, version) | ||
| .unwrap_or(false), | ||
| ReadinessMethod::Both => { | ||
| if config.prefer_index { | ||
| match self.check_index_visibility(crate_name, version) { | ||
| Ok(true) => true, | ||
| _ => self.version_exists(crate_name, version).unwrap_or(false), | ||
| } | ||
| } else { | ||
| match self.version_exists(crate_name, version) { | ||
| Ok(true) => true, | ||
| _ => self | ||
| .check_index_visibility(crate_name, version) | ||
| .unwrap_or(false), | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| evidence.push(ReadinessEvidence { | ||
| attempt, | ||
| visible, | ||
| timestamp: Utc::now(), | ||
| delay_before: jittered_delay, | ||
| }); | ||
|
|
||
| if visible { | ||
| return Ok((true, evidence)); | ||
| } | ||
|
|
||
| if start.elapsed() >= config.max_total_wait { | ||
| return Ok((false, evidence)); | ||
| } | ||
|
|
||
| let base_delay = config.poll_interval; | ||
| let exponential_delay = | ||
| base_delay.saturating_mul(2_u32.saturating_pow(attempt.saturating_sub(1).min(16))); | ||
| let capped_delay = exponential_delay.min(config.max_delay); | ||
| let jitter_range = config.jitter_factor; | ||
| let jitter = 1.0 + (rand::random::<f64>() * 2.0 * jitter_range - jitter_range); | ||
| let next_delay = | ||
| Duration::from_millis((capped_delay.as_millis() as f64 * jitter).round() as u64); | ||
|
|
||
| std::thread::sleep(next_delay); | ||
| } |
There was a problem hiding this comment.
Backoff delay recorded in evidence differs from the delay actually slept.
The jittered_delay computed on lines 135-145 (using attempt.saturating_sub(2)) is recorded in the evidence, but the actual sleep delay is computed on lines 184-191 (using attempt.saturating_sub(1)). These formulas produce different values for the same attempt number, making the evidence inaccurate.
Additionally, the entire backoff computation (exponential with jitter, cap) is duplicated between lines 135-145, lines 184-191, and calculate_backoff_delay (lines 200-216). Extract a shared helper.
Proposed fix: unify the delay computation
+ fn compute_delay(&self, poll_interval: Duration, max_delay: Duration, attempt: u32, jitter_factor: f64) -> Duration {
+ let pow = attempt.saturating_sub(1).min(16);
+ let exponential_delay = poll_interval.saturating_mul(2_u32.saturating_pow(pow));
+ let capped_delay = exponential_delay.min(max_delay);
+ let jitter = 1.0 + (rand::random::<f64>() * 2.0 * jitter_factor - jitter_factor);
+ Duration::from_millis((capped_delay.as_millis() as f64 * jitter).round() as u64)
+ }Then use it in both the evidence recording and the actual sleep.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper/src/registry_micro.rs` around lines 132 - 194, The recorded
jittered_delay and the actual sleep use different backoff formulas
(attempt.saturating_sub(2) vs saturating_sub(1)), and the backoff logic is
duplicated; unify them by using the existing calculate_backoff_delay helper for
both evidence and sleeping. Replace the inline jittered_delay computation in the
loop with a call to calculate_backoff_delay(config, attempt) (or adapt the
helper signature if needed), use that same returned Duration for both the
ReadinessEvidence.delay_before and for the actual sleep (replace next_delay),
and remove the duplicate backoff math so all backoff behavior is driven by
calculate_backoff_delay; keep the special-case of attempt == 1 returning
Duration::ZERO inside the helper if required.
8c76512 to
84749c6
Compare
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/shipper/src/cargo.rs (1)
109-145: 🧹 Nitpick | 🔵 Trivial
output.durationis discarded;start.elapsed()double-counts outer overhead.
process::run_command_with_timeoutcaptures its ownstartand returnsoutput.durationfor the process execution time.cargo_publish(and the two dry-run helpers at lines 154 and 194) take a secondInstant::now()before building args and then usestart.elapsed()instead ofoutput.duration. The outer duration includes a few microseconds of arg-building overhead and is slightly less precise for profiling. Consider usingoutput.durationdirectly:- let start = Instant::now(); let mut args: Vec<&str> = Vec::new(); // ... let output = process::run_command_with_timeout(&cargo_program(), &args, workspace_root, timeout) .context("failed to execute cargo publish; is Cargo installed?")?; let exit_code = output.exit_code; let stdout = output.stdout; let stderr = output.stderr; let timed_out = output.timed_out; - let duration = start.elapsed(); + let duration = output.duration; Ok(CargoOutput { exit_code, stdout_tail: tail_lines(&stdout, output_lines), stderr_tail: tail_lines(&stderr, output_lines), duration, timed_out, })Apply the same change to
cargo_publish_dry_run_workspace(line 154) andcargo_publish_dry_run_package(line 194).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/shipper/src/cargo.rs` around lines 109 - 145, The duration computed with start.elapsed() in cargo_publish (and the dry-run helpers cargo_publish_dry_run_workspace and cargo_publish_dry_run_package) double-counts outer overhead and discards the process's own timing; change these functions to use the duration returned in output.duration (from process::run_command_with_timeout) instead of computing a new Instant::now() / start.elapsed(), and return that output.duration in the CargoOutput struct so the reported duration reflects the actual process execution time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.config/nextest.toml:
- Around line 14-15: The comment above the test runner setting is incorrect:
"test-threads" controls parallel thread count, not per-test timeouts; either
update the comment to reflect that it sets parallelism (e.g., "Number of test
threads / parallelism") or, if the intent was to set a timeout, replace or add
the appropriate timeout key such as "slow-timeout" or "leak-timeout" instead of
"test-threads"; locate the "test-threads" entry and either change its comment
text to mention threads/parallelism or add/replace it with the correct timeout
key (slow-timeout or leak-timeout) as needed.
In @.github/workflows/ci.yml:
- Around line 84-85: The "Run doc tests" GitHub Actions step currently invokes
cargo test with "cargo test --workspace --doc" which skips feature-gated doc
examples; update the step named "Run doc tests" to include the --all-features
flag so it runs "cargo test --workspace --doc --all-features" (or equivalent
ordering) to ensure doc tests behind #[cfg(feature = "...")] are exercised.
- Around line 193-198: The GitHub Actions step named "Upload to Codecov" is
using codecov/codecov-action@v4; update the step to use
codecov/codecov-action@v5 (replace the uses value) to gain the v5 reliability
fixes and tokenless upload support; keep the existing files: lcov.info config
and, if your repo/org supports tokenless uploads, remove the token: ${{
secrets.CODECOV_TOKEN }} input, otherwise keep the token input unchanged.
- Around line 329-337: The CI release step uses the command "cargo build
--release" which will fail for a workspace-only manifest; update the Build
release step to run "cargo build --release --workspace" so all workspace members
are compiled (locate the step named "Build release" and modify its run command
accordingly).
In `@crates/shipper-cargo/src/lib.rs`:
- Around line 506-516: The impl From<&Package> for PackageInfo currently
hardcodes is_workspace_member: true; change this to a safe default (e.g., false)
or refactor the conversion to accept workspace context (e.g., pass
&HashSet<String> or &HashSet<PackageId> of metadata.workspace_members) so the
flag can be computed correctly; then update the caller (e.g., WorkspaceMetadata
construction logic) to set PackageInfo::is_workspace_member by checking whether
pkg.id or pkg.manifest_path is in metadata.workspace_members rather than relying
on the From impl.
- Around line 467-488: build_dependency_graph is pulling package entries for
transitive external crates from self.metadata.packages, which causes
topological_order/visit_package to include and attempt to publish external crate
names; restrict the lookup to workspace members (or call .no_deps() when
building MetadataCommand instead) so only workspace package names appear in the
graph: in build_dependency_graph, when mapping dependencies, only map dep.name
to a package if that package is one of the workspace/publishable packages (use
publishable_packages() or the metadata's workspace member list/ids to filter) so
topological_order and visit_package only see workspace package names.
- Around line 53-154: The cargo_publish function duplicates process timeout
logic; remove the manual poll-and-kill loop and instead depend on the
shipper-process crate and call its helper (e.g.,
shipper_process::run_command_with_timeout or the crate's equivalent) to run the
prepared Command with the optional timeout; ensure you add shipper-process to
Cargo.toml, invoke the helper with the constructed Command (include current_dir
and args from cargo_publish), then map the helper's result to the CargoOutput
struct (exit_code, stdout_tail via tail_lines(&stdout, output_lines),
stderr_tail, duration, and timed_out) so behavior stays identical but uses the
centralized timeout/backpressure implementation.
In `@crates/shipper-cli/src/main.rs`:
- Around line 143-149: Remove the sensitive --webhook-secret CLI flag from the
Args definition and instead read the webhook signing secret from an environment
variable (e.g., SHIPPER_WEBHOOK_SECRET) at runtime; update references to the
webhook_secret field in main.rs to fetch
std::env::var("SHIPPER_WEBHOOK_SECRET").ok() (or a helper like get_env_secret)
and treat webhook_secret as Option<String> derived from the env lookup, and
update help/docs to reflect the new env-only input method so the secret is no
longer exposed in process listings.
- Around line 426-438: The ProgressReporter is created and initialized
(ProgressReporter::new and progress.set_package) but engine::run_publish is
invoked without it, so intermediate updates never occur; modify
engine::run_publish to accept a mutable ProgressReporter (or a callback/trait
object) and pass &mut progress (or a closure) from the Publish branch so the
engine can call progress.set_package / progress.increment during publish, then
call progress.finish() after run_publish returns; update any call sites and the
run_publish signature accordingly (or alternatively defer creating progress
until engine reports support) to ensure ProgressReporter receives updates.
- Around line 694-707: The parallel estimate currently uses 30 *
number_of_levels and ignores how many packages are in each level and any
concurrency limit; update the computation of estimated_parallel_secs to sum
per-level durations using the package count and max concurrency (for each level
compute ceil(level.packages.len() / max_concurrent) * 30 and sum them). Locate
the block using variables levels and estimated_parallel_secs in main.rs (the
code that prints "Estimated time (parallel)"), add or reference the existing
max_concurrent value (or derive it from the runner settings), and replace the
simple levels.iter().map(|_l| 30).sum() with a per-level calculation that uses
level.packages.len() and max_concurrent (use integer-safe ceiling via (n +
max_concurrent - 1) / max_concurrent).
- Around line 1211-1215: The CircleCI YAML generator is printing a GitHub
Actions-style expression which will set the env var to a literal string; remove
the unnecessary environment block output so CircleCI uses the previously
declared context: specifically delete or stop emitting the println! that prints
" CARGO_REGISTRY_TOKEN: ${{{{ CARGO_REGISTRY_TOKEN }}}}" (and the
surrounding "environment:" lines printed via println!) so the secret from the
existing context: cargo-registry is used instead.
In `@crates/shipper-types/src/lib.rs`:
- Around line 550-585: RuntimeOptions currently references
shipper_retry::RetryStrategyType and shipper_retry::PerErrorConfig inline which
is inconsistent with other cross-crate types; add a top-level use statement like
use shipper_retry::{RetryStrategyType, PerErrorConfig}; then update the
RuntimeOptions struct to use the unqualified types RetryStrategyType and
PerErrorConfig (symbols: RuntimeOptions, RetryStrategyType, PerErrorConfig,
shipper_retry) to match how WebhookConfig/EncryptionSettings are imported.
---
Outside diff comments:
In `@crates/shipper/src/cargo.rs`:
- Around line 109-145: The duration computed with start.elapsed() in
cargo_publish (and the dry-run helpers cargo_publish_dry_run_workspace and
cargo_publish_dry_run_package) double-counts outer overhead and discards the
process's own timing; change these functions to use the duration returned in
output.duration (from process::run_command_with_timeout) instead of computing a
new Instant::now() / start.elapsed(), and return that output.duration in the
CargoOutput struct so the reported duration reflects the actual process
execution time.
---
Duplicate comments:
In @.config/nextest.toml:
- Around line 4-6: The comment above the [profile.ci] setting is misleading: it
reads "Fail fast on CI to save time" while the actual setting is fail-fast =
false. Either update the comment to reflect that fail-fast is disabled (e.g.,
"Do not fail fast on CI to collect all test results") or flip the boolean to
fail-fast = true if the intent is to enable fail-fast; modify the text or the
value in the .config/nextest.toml file around the profile.ci section accordingly
to keep comment and setting consistent.
- Around line 28-31: The file ends without a trailing newline (affecting the
[profile.nightly] block); open the file containing the [profile.nightly] section
and add a single newline character at the end of the file (after the
test-threads = "num-cpus" line) so the file ends with a trailing newline for
POSIX compliance.
In @.github/workflows/ci.yml:
- Around line 114-115: The CI job "Check with MSRV" currently runs cargo check
--workspace which misses feature-gated code; update that job to run cargo check
--workspace --all-features (or cargo check --workspace --features micro-all if
you prefer explicit feature set) so MSRV checks include feature-gated crates and
catches APIs used only behind features; adjust the job step named "Check with
MSRV" to invoke the new cargo check command.
- Around line 230-233: The workflow's fuzz-smoke job currently excludes
scheduled runs via the conditional "if: github.event_name != 'schedule'", which
prevents the nightly cron from running the intended quick fuzz tests; fix this
by either removing that conditional from the fuzz-smoke job so it runs on
schedule as well, or create a separate nightly job (e.g., fuzz-smoke-nightly)
that runs only when github.event_name == 'schedule' and otherwise keep the
PR-specific fuzz-smoke for non-schedule events—update the job name/reference
'fuzz-smoke' and the conditional line accordingly.
In `@Cargo.toml`:
- Line 36: The Cargo.toml proptest dependency is pinned to a non-existent
version "1.10.0" which breaks resolution; update the proptest entry to use the
published version (e.g., "1.9.0" or a compatible semver requirement like
"^1.9.0") so that the proptest dependency line in Cargo.toml resolves correctly.
In `@crates/shipper-cargo/src/lib.rs`:
- Around line 416-431: The topological_order method uses HashSet/HashMap causing
non-deterministic iteration; replace unordered collections with deterministic
ones (BTreeSet/BTreeMap) so results are reproducible. Change local vars visited
and visiting to BTreeSet, ensure build_dependency_graph returns/uses a BTreeMap
(or convert its result to a BTreeMap before iterating), and iterate
publishable_packages in a deterministic order (e.g., sort by package.name or
collect into a BTreeSet) before calling visit_package(&name, &dep_graph, &mut
visited, &mut visiting, &mut order) so topological_order and visit_package
produce stable output across runs.
- Around line 79-132: The polling loop deadlocks because stdout/stderr are only
read after try_wait reports exit; instead, take child.stdout and child.stderr
immediately after spawn and spawn background threads to continuously read_to_end
into shared buffers while the loop polls try_wait; on Some(status) and on
timeout join those reader threads, collect their byte buffers into
stdout_bytes/stderr_bytes, and then proceed with constructing the return tuple
(handle ownership of child.stdout/child.stderr by taking them before the loop,
use thread::spawn to read into owned Vec<u8>, and join handles to retrieve the
data in both the normal-exit and timeout branches).
- Around line 648-656: The test workspace_metadata_topological_order currently
asserts a tautology; replace it with concrete validation: call
WorkspaceMetadata::load_from_current_dir() and then
metadata.topological_order(), and if result is Ok(order) assert that order
contains exactly the same package ids as metadata.packages (order.len() ==
metadata.packages.len() and the sets are equal) and that for every package in
metadata.packages each of its workspace dependencies appears earlier in order
(for each dep, position(dep) < position(package)); if result is Err, assert the
error represents a circular-dependency failure (or otherwise matches the
expected error type/message) instead of using a vacuous is_ok() || is_err()
check so the test verifies real ordering properties.
In `@crates/shipper-cli/src/main.rs`:
- Around line 1276-1277: The function signature for run_clean currently takes a
&PathBuf but should accept a &Path; update the signature of run_clean to take
state_dir: &Path (and import or use std::path::Path), then update all call sites
that pass a PathBuf or &PathBuf to pass state_dir.as_path() or a &Path reference
as needed; also check any internal uses of state_dir that rely on
PathBuf-specific APIs and adjust them to use Path methods or perform conversions
where necessary.
- Around line 151-157: The CLI exposes the sensitive encrypt_passphrase via the
--encrypt-passphrase flag (encrypt_passphrase) which leaks secrets in process
listings; remove the public CLI option from the struct in main.rs and instead
obtain the passphrase at runtime by first checking the SHIPPER_ENCRYPT_KEY
environment variable and, if absent, prompting securely (e.g., use
rpassword::read_password or similar) when encrypt is true; update any code paths
that referenced encrypt_passphrase to use the new env-or-prompt value and ensure
no plaintext passphrase is logged or stored.
In `@crates/shipper-types/src/lib.rs`:
- Around line 36-60: serialize_duration currently casts duration.as_millis()
(u128) to u64 with a truncating "as" cast; change it to use a fallible
conversion (e.g., u128::try_into or .try_into()) and return a serialization
error on overflow so you don't silently truncate — update the serialize_duration
function to attempt converting duration.as_millis() to u64 and map any
TryIntoError into S::Error via serde::ser::Error::custom with a clear message.
- Line 1551: Remove the redundant #[cfg(test)] attribute on the inner module
declaration mod proptests because it is already enclosed by the outer
#[cfg(test)] mod tests; locate the inner module named proptests and delete the
#[cfg(test)] above it so the module remains included only once under the outer
test cfg.
- Around line 127-139: In get_index_base, the chained .replace calls on
self.api_base can perform multiple unintended substitutions; instead use
.replacen(..., 1) to limit each substitution to a single occurrence. Update the
api_base transformation so the "https://" replacement uses .replacen("https://",
"https://index.", 1) and the "http://" replacement uses .replacen("http://",
"http://index.", 1), keeping the existing strip_prefix("sparse+") handling for
self.index_base intact.
- Around line 83-93: Add equality derivations to the data structs by updating
the struct attributes for Registry (and other similar types) to derive PartialEq
and Eq in addition to Debug, Clone, Serialize, Deserialize; specifically modify
the Registry derive list (#[derive(...)] on the Registry struct) to include
PartialEq and Eq so instances can be compared in tests and logic that require
equality semantics.
- Around line 1133-1138: The PublishEvent struct's package field is always empty
for non-package events; change pub package: String to an optional field pub
package: Option<String> in the PublishEvent definition and update Serde handling
(e.g., allow None / skip_serializing_if = "Option::is_none" or a default) and
any constructors or places that build PublishEvent to set Some("name@version")
only for package-related EventType values (and None otherwise). Ensure all code
that reads package now handles Option<String> (unwrapping or pattern-matching)
and update any tests/serializations that assume a non-empty String.
- Around line 1807-1876: The tests are exercising test-local helpers that
diverge from production behavior: replace the local helpers by reusing the
production implementations so tests validate real code—call the production
index-path function (the one used in crates/shipper/src/registry.rs that uses
to_ascii_lowercase()) instead of calculate_index_path_for_crate, and import/use
the production schema-parsing routine instead of parse_schema_version_for_test;
if the production functions are private, make them test-visible (pub(crate) or
add #[cfg(test)] pub(crate) wrappers) so the property tests use the exact
production logic and casing semantics.
- Around line 919-929: PackageReceipt currently declares duration_ms as u128
which is inconsistent with EventType::PackagePublished::duration_ms and
EventType::ReadinessComplete::duration_ms (both u64) and can break JSON
consumers; change PackageReceipt::duration_ms from u128 to u64, update any
constructors, conversions, or arithmetic that produce/consume duration_ms (e.g.,
places that compute duration in PackageReceipt or populate it from EventType
variants) to cast/normalize to u64 safely, and run serialization tests to ensure
serde output remains compatible.
- Around line 356-438: ReadinessConfig currently mixes two Duration
serialization approaches (the #[serde_as] usage at the struct level and the
custom serialize_duration/deserialize_duration functions on individual fields),
which is intentional but not documented; update the codebase by adding a clear
comment explaining why both strategies are present and which fields use the
custom (serialize_duration/deserialize_duration) functions versus serde_as
handling so future reviewers understand the design; reference the
ReadinessConfig struct, the serde_as attribute, and the
serialize_duration/deserialize_duration helper symbols in that comment (either
on the struct or as a nearby crate-level doc) so maintainers can locate and
audit the behavior easily.
- Around line 696-733: The group_by_levels implementation is duplicated; in the
implementation in plan.rs remove the duplicated body and forward/delegate to the
canonical group_by_levels in the shipper-types crate by calling that exported
method instead (import the type/trait from shipper-types as needed and return
the result of its group_by_levels call rather than reimplementing the logic),
keeping the same signature so callers are unchanged and relying on the single
canonical implementation named group_by_levels.
- Around line 1556-1610: The test is doing manual field-by-field comparisons
because many types lack PartialEq/Eq; update the type definitions (add
#[derive(PartialEq, Eq, Debug, Clone)] or at least PartialEq where Eq is
invalid) for PreflightReport, PreflightPackage, Registry, PlannedPackage,
PackageEvidence, AttemptEvidence, ReadinessEvidence, EnvironmentFingerprint,
GitContext, PackageReceipt, Receipt, PublishEvent, ReadinessConfig (derive
PartialEq but not Eq because jitter_factor: f64), ParallelConfig, ReleasePlan,
ExecutionState, and PackageProgress so the proptest can use assert_eq!(parsed,
report); note DateTime<Utc> already implements PartialEq so comparing types
containing it is fine.
| # Timeout for individual tests | ||
| test-threads = "num-cpus" |
There was a problem hiding this comment.
Misleading comment: this sets thread count, not a timeout.
The comment says "Timeout for individual tests" but the actual setting is test-threads, which controls parallelism. If a per-test timeout is intended, use slow-timeout or leak-timeout instead; otherwise, fix the comment.
Proposed fix
-# Timeout for individual tests
-test-threads = "num-cpus"
+# Parallelism: use all available CPUs
+test-threads = "num-cpus"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Timeout for individual tests | |
| test-threads = "num-cpus" | |
| # Parallelism: use all available CPUs | |
| test-threads = "num-cpus" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.config/nextest.toml around lines 14 - 15, The comment above the test runner
setting is incorrect: "test-threads" controls parallel thread count, not
per-test timeouts; either update the comment to reflect that it sets parallelism
(e.g., "Number of test threads / parallelism") or, if the intent was to set a
timeout, replace or add the appropriate timeout key such as "slow-timeout" or
"leak-timeout" instead of "test-threads"; locate the "test-threads" entry and
either change its comment text to mention threads/parallelism or add/replace it
with the correct timeout key (slow-timeout or leak-timeout) as needed.
| - name: Run doc tests | ||
| run: cargo test --workspace --doc |
There was a problem hiding this comment.
Doc tests missing --all-features flag.
Line 77 runs nextest with --all-features, but the doc-test step on line 85 uses only --workspace --doc, skipping feature-gated doc examples. This means doc tests behind #[cfg(feature = "micro-*")] won't be exercised.
Proposed fix
- name: Run doc tests
- run: cargo test --workspace --doc
+ run: cargo test --workspace --all-features --doc🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 84 - 85, The "Run doc tests" GitHub
Actions step currently invokes cargo test with "cargo test --workspace --doc"
which skips feature-gated doc examples; update the step named "Run doc tests" to
include the --all-features flag so it runs "cargo test --workspace --doc
--all-features" (or equivalent ordering) to ensure doc tests behind
#[cfg(feature = "...")] are exercised.
| - name: Upload to Codecov | ||
| uses: codecov/codecov-action@v4 | ||
| with: | ||
| files: lcov.info | ||
| fail_ci_if_error: false | ||
| token: ${{ secrets.CODECOV_TOKEN }} |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🌐 Web query:
codecov-action v5 release date 2024 2025
💡 Result:
- codecov/codecov-action v5.0.0 (first v5 stable) was released on Nov 14, 2024. [1][2]
- v5 releases in 2025 include (examples): v5.2.0 (Jan 22, 2025), v5.4.0 (Feb 26, 2025), and v5.5.2 (Dec 9, 2025). [3][4]
Sources
[1] https://github.com/codecov/codecov-action/tags?after=v5.0.3
[2] https://github.com/codecov/codecov-action/releases/tag/v5.0.0
[3] https://github.com/codecov/codecov-action/tags?after=v5.3.1
[4] https://github.com/codecov/codecov-action/tags
🌐 Web query:
codecov-action v5 new features improvements tokenless public repos
💡 Result:
codecov/codecov-action v5 — what’s new / improved (incl. tokenless for public repos)
- Uses the “Codecov Wrapper” (instead of directly calling the CLI), intended to let the Action receive updater/fixes faster. [1]
- Tokenless uploads for public repos are supported via an org-level setting: in Codecov, go to your org Settings → Global Upload Token and set tokens to Not required (admins only). This enables uploads (including from contributors) without needing access to a
CODECOV_TOKENsecret. [1][2] - Public-repo token rules clarified: tokens may still be required for things like protected-branch uploads unless token auth is disabled; tokenless is allowed for unprotected branches (e.g.,
forkname:main) and when token auth is disabled for public repos. [2] - Inputs/arguments changes in v5 (migration notes):
file→ deprecated in favor offilesplugin→ deprecated in favor ofplugins- New inputs include:
binary,gcov_args,gcov_executable,gcov_ignore,gcov_include,report_type,skip_validation,swift_project. [1]
- Alternative to upload tokens: OIDC support via
use_oidc: true(requirespermissions: id-token: write). [3]
Sources: GitHub Marketplace listing (v5 migration notes) [1], Codecov docs on tokens & “Disabling Token Authentication for Public Repositories” (>v5 action) [2], codecov-action README (OIDC) [3].
Upgrade codecov/codecov-action to v5 for improved reliability.
v5.0.0 was released in November 2024 with enhanced upload reliability and support for tokenless uploads on public repositories (via org-level settings). The current files: lcov.info syntax is compatible with v5.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 193 - 198, The GitHub Actions step
named "Upload to Codecov" is using codecov/codecov-action@v4; update the step to
use codecov/codecov-action@v5 (replace the uses value) to gain the v5
reliability fixes and tokenless upload support; keep the existing files:
lcov.info config and, if your repo/org supports tokenless uploads, remove the
token: ${{ secrets.CODECOV_TOKEN }} input, otherwise keep the token input
unchanged.
| - name: Build release | ||
| run: cargo build --release | ||
|
|
||
| - name: Upload release binary | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: shipper-linux-x86_64 | ||
| path: target/release/shipper | ||
| retention-days: 7 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check the workflow file to see lines 304 and 329-337
git ls-files .github/workflows/ci.ymlRepository: EffortlessMetrics/shipper
Length of output: 93
🏁 Script executed:
# Read the CI workflow file around the relevant lines
wc -l .github/workflows/ci.ymlRepository: EffortlessMetrics/shipper
Length of output: 97
🏁 Script executed:
# Check root Cargo.toml for [profile.release] and binary name
cat -n Cargo.toml | head -50Repository: EffortlessMetrics/shipper
Length of output: 1178
🏁 Script executed:
# Read the CI workflow file around lines 300-340 to see the cross-platform job and release build
sed -n '300,340p' .github/workflows/ci.ymlRepository: EffortlessMetrics/shipper
Length of output: 1230
🏁 Script executed:
# Check if there's a [profile.release] section in root Cargo.toml more thoroughly
sed -n '38,50p' Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 110
🏁 Script executed:
# Check which crate/binary is named "shipper"
fd 'Cargo.toml' | xargs grep -l '\[\[bin\]\]' -A 5 | head -5Repository: EffortlessMetrics/shipper
Length of output: 114
🏁 Script executed:
# Check the main crates/shipper/Cargo.toml for binary name
cat crates/shipper/Cargo.toml | grep -A 5 '\[\[bin\]\]'Repository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
# Verify the binary name explicitly
cat crates/shipper/Cargo.toml | grep -E '^\[package\]|^name|^version' -A 2Repository: EffortlessMetrics/shipper
Length of output: 170
🏁 Script executed:
# Check crates/shipper-cli for binary name since it appeared in the grep results
cat crates/shipper-cli/Cargo.toml | grep -E '^\[|^name'Repository: EffortlessMetrics/shipper
Length of output: 177
🏁 Script executed:
# Check what binaries are actually built - look for [[bin]] in workspace crates
rg '\[\[bin\]\]' -A 3 --type tomlRepository: EffortlessMetrics/shipper
Length of output: 529
🏁 Script executed:
# Verify the actual binary path after cargo build --release in root
# by checking if there are other binaries that might be built
fd Cargo.toml --exec grep -l 'name = "shipper"' {} \;Repository: EffortlessMetrics/shipper
Length of output: 128
🏁 Script executed:
# Check what the default build target is for cargo build --release
# (i.e., what gets built if no --workspace is specified)
sed -n '1,25p' Cargo.toml | grep -E 'members|default-members'Repository: EffortlessMetrics/shipper
Length of output: 80
🏁 Script executed:
# Verify that crates/shipper is a library and crates/shipper-cli is the main binary
rg '\[lib\]' -A 2 crates/shipper/Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
# Check if there's a [package] section in the root Cargo.toml
grep '\[package\]' Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
# Check the entire root Cargo.toml structure
tail -20 Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 399
🏁 Script executed:
# Verify what happens with cargo build when there's a workspace with no root package
# Look for any features defined in workspace
rg 'features' Cargo.toml -B 2 -A 5 | head -30Repository: EffortlessMetrics/shipper
Length of output: 208
🏁 Script executed:
# Check if crates/shipper-cli has significant feature gates
cat crates/shipper-cli/Cargo.toml | grep -A 20 '\[features\]'Repository: EffortlessMetrics/shipper
Length of output: 763
🏁 Script executed:
# Also check crates/shipper for features since it's a dependency
cat crates/shipper/Cargo.toml | grep -A 20 '\[features\]'Repository: EffortlessMetrics/shipper
Length of output: 1140
🏁 Script executed:
# Check the complete root Cargo.toml to ensure we have the full picture
cat Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 898
🏁 Script executed:
# Check if cargo build --release would work without --workspace on a workspace with no root package
# by seeing if there's any default-members config
rg 'default-members' Cargo.tomlRepository: EffortlessMetrics/shipper
Length of output: 51
🏁 Script executed:
# Verify the exact path where the binary ends up after cargo build --release
# Check if target/release/shipper exists or if it's in a different location
# by looking at what crates/shipper-cli builds to
cat crates/shipper-cli/Cargo.toml | head -20Repository: EffortlessMetrics/shipper
Length of output: 754
🏁 Script executed:
# Determine what cargo build --release does on a workspace-only manifest with no root package or default-members
# Check Cargo documentation behavior and whether the workflow would fail
python3 << 'EOF'
import json
# According to Cargo docs, when you have a workspace with no root package:
# - cargo build without --workspace will fail
# - cargo build --workspace will build all workspace members
# - cargo build -p <package> will build a specific package
# The release job in the CI does: cargo build --release (no --workspace, no package specified)
# This should fail because there's no root package and no default-members
# Let's verify by checking if there could be a default-members we missed
print("Analysis: Workspace without root package, no default-members")
print("Expected behavior of 'cargo build --release': FAIL")
print("Expected behavior of 'cargo build --workspace --release': SUCCESS (builds all members)")
print("")
print("If the CI workflow is functional as-is, one of these must be true:")
print("1. There IS a root [package] (but we didn't see it)")
print("2. There IS default-members (but grep didn't find it)")
print("3. The workflow is broken/never runs this job")
EOFRepository: EffortlessMetrics/shipper
Length of output: 487
🏁 Script executed:
# Double-check the entire Cargo.toml file size and content to ensure we didn't miss anything
wc -l Cargo.toml && echo "---" && cat Cargo.toml | tail -5Repository: EffortlessMetrics/shipper
Length of output: 149
🏁 Script executed:
# Check the cross-platform job definition to verify if it really uses --all-features
sed -n '290,310p' .github/workflows/ci.ymlRepository: EffortlessMetrics/shipper
Length of output: 848
🏁 Script executed:
# Let's trace through the workflow to see all build jobs and their commands
grep -n 'cargo build' .github/workflows/ci.ymlRepository: EffortlessMetrics/shipper
Length of output: 178
Release build missing --workspace flag; LTO and stripping already configured correctly.
The release build (line 330) omits --workspace, which is required on this workspace-only manifest without a root package or default-members. The cross-platform job correctly includes --workspace (line 304), ensuring all workspace members are compiled. Without it, the release build will fail.
However, LTO and stripping are already properly configured in the root Cargo.toml:
[profile.release]
strip = true
lto = true
Add --workspace to the release build command:
Proposed fix
- name: Build release
- run: cargo build --release
+ run: cargo build --workspace --release🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ci.yml around lines 329 - 337, The CI release step uses
the command "cargo build --release" which will fail for a workspace-only
manifest; update the Build release step to run "cargo build --release
--workspace" so all workspace members are compiled (locate the step named "Build
release" and modify its run command accordingly).
| pub fn cargo_publish( | ||
| workspace_root: &Path, | ||
| package_name: &str, | ||
| registry_name: &str, | ||
| allow_dirty: bool, | ||
| no_verify: bool, | ||
| output_lines: usize, | ||
| timeout: Option<Duration>, | ||
| ) -> Result<CargoOutput> { | ||
| let start = Instant::now(); | ||
| let mut cmd = Command::new(cargo_program()); | ||
| cmd.arg("publish").arg("-p").arg(package_name); | ||
|
|
||
| if !registry_name.trim().is_empty() && registry_name != "crates-io" { | ||
| cmd.arg("--registry").arg(registry_name); | ||
| } | ||
|
|
||
| if allow_dirty { | ||
| cmd.arg("--allow-dirty"); | ||
| } | ||
| if no_verify { | ||
| cmd.arg("--no-verify"); | ||
| } | ||
|
|
||
| cmd.current_dir(workspace_root); | ||
|
|
||
| let (exit_code, stdout, stderr, timed_out) = if let Some(timeout_dur) = timeout { | ||
| let mut child = cmd | ||
| .stdout(std::process::Stdio::piped()) | ||
| .stderr(std::process::Stdio::piped()) | ||
| .spawn() | ||
| .context("failed to execute cargo publish; is Cargo installed?")?; | ||
|
|
||
| let deadline = Instant::now() + timeout_dur; | ||
| loop { | ||
| match child.try_wait().context("failed to poll cargo process")? { | ||
| Some(status) => { | ||
| let mut stdout_bytes = Vec::new(); | ||
| let mut stderr_bytes = Vec::new(); | ||
| if let Some(mut out) = child.stdout.take() { | ||
| let _ = out.read_to_end(&mut stdout_bytes); | ||
| } | ||
| if let Some(mut err) = child.stderr.take() { | ||
| let _ = err.read_to_end(&mut stderr_bytes); | ||
| } | ||
| break ( | ||
| status.code().unwrap_or(-1), | ||
| String::from_utf8_lossy(&stdout_bytes).to_string(), | ||
| String::from_utf8_lossy(&stderr_bytes).to_string(), | ||
| false, | ||
| ); | ||
| } | ||
| None => { | ||
| if Instant::now() >= deadline { | ||
| let _ = child.kill(); | ||
| let _ = child.wait(); | ||
| let mut stdout_bytes = Vec::new(); | ||
| let mut stderr_bytes = Vec::new(); | ||
| if let Some(mut out) = child.stdout.take() { | ||
| let _ = out.read_to_end(&mut stdout_bytes); | ||
| } | ||
| if let Some(mut err) = child.stderr.take() { | ||
| let _ = err.read_to_end(&mut stderr_bytes); | ||
| } | ||
| let mut stderr_str = String::from_utf8_lossy(&stderr_bytes).to_string(); | ||
| stderr_str.push_str(&format!( | ||
| "\ncargo publish timed out after {}", | ||
| humantime::format_duration(timeout_dur) | ||
| )); | ||
| break ( | ||
| -1, | ||
| String::from_utf8_lossy(&stdout_bytes).to_string(), | ||
| stderr_str, | ||
| true, | ||
| ); | ||
| } | ||
| std::thread::sleep(Duration::from_millis(100)); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| let out = cmd | ||
| .output() | ||
| .context("failed to execute cargo publish; is Cargo installed?")?; | ||
| ( | ||
| out.status.code().unwrap_or(-1), | ||
| String::from_utf8_lossy(&out.stdout).to_string(), | ||
| String::from_utf8_lossy(&out.stderr).to_string(), | ||
| false, | ||
| ) | ||
| }; | ||
|
|
||
| let duration = start.elapsed(); | ||
|
|
||
| Ok(CargoOutput { | ||
| exit_code, | ||
| stdout_tail: tail_lines(&stdout, output_lines), | ||
| stderr_tail: tail_lines(&stderr, output_lines), | ||
| duration, | ||
| timed_out, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate timeout implementation — shipper-process crate already owns this logic.
crates/shipper/src/process.rs (used by crates/shipper/src/cargo.rs after refactoring) and process_micro.rs (which wraps shipper_process::run_command_with_timeout) demonstrate a dedicated shipper-process crate. shipper-cargo reimplements the entire poll-and-kill loop inline. If shipper-cargo's Cargo.toml gains a shipper-process dependency, cargo_publish becomes:
- let (exit_code, stdout, stderr, timed_out) = if let Some(timeout_dur) = timeout {
- let mut child = cmd
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .spawn()
- .context("failed to execute cargo publish; is Cargo installed?")?;
- // ... 40 lines of poll loop ...
- } else {
- let out = cmd.output()
- .context("failed to execute cargo publish; is Cargo installed?")?;
- (out.status.code().unwrap_or(-1),
- String::from_utf8_lossy(&out.stdout).to_string(),
- String::from_utf8_lossy(&out.stderr).to_string(),
- false)
- };
+ let prog = cargo_program();
+ let output = shipper_process::run_command_with_timeout(
+ &prog, &args, workspace_root, timeout,
+ ).context("failed to execute cargo publish; is Cargo installed?")?;
+ let (exit_code, stdout, stderr, timed_out) =
+ (output.exit_code, output.stdout, output.stderr, output.timed_out);This also fixes the backpressure deadlock at one place rather than in every crate that invokes subprocesses.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cargo/src/lib.rs` around lines 53 - 154, The cargo_publish
function duplicates process timeout logic; remove the manual poll-and-kill loop
and instead depend on the shipper-process crate and call its helper (e.g.,
shipper_process::run_command_with_timeout or the crate's equivalent) to run the
prepared Command with the optional timeout; ensure you add shipper-process to
Cargo.toml, invoke the helper with the constructed Command (include current_dir
and args from cargo_publish), then map the helper's result to the CargoOutput
struct (exit_code, stdout_tail via tail_lines(&stdout, output_lines),
stderr_tail, duration, and timed_out) so behavior stays identical but uses the
centralized timeout/backpressure implementation.
| /// Webhook URL to send publish event notifications to | ||
| #[arg(long, global = true)] | ||
| webhook_url: Option<String>, | ||
|
|
||
| /// Optional secret for signing webhook payloads | ||
| #[arg(long, global = true)] | ||
| webhook_secret: Option<String>, |
There was a problem hiding this comment.
--webhook-secret is visible in process listings, same risk as --encrypt-passphrase.
This CLI flag exposes the webhook signing secret via ps aux / /proc/*/cmdline. Consider supporting an environment variable (e.g., SHIPPER_WEBHOOK_SECRET) as the sole input method, consistent with the recommendation already made for --encrypt-passphrase.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 143 - 149, Remove the sensitive
--webhook-secret CLI flag from the Args definition and instead read the webhook
signing secret from an environment variable (e.g., SHIPPER_WEBHOOK_SECRET) at
runtime; update references to the webhook_secret field in main.rs to fetch
std::env::var("SHIPPER_WEBHOOK_SECRET").ok() (or a helper like get_env_secret)
and treat webhook_secret as Option<String> derived from the env lookup, and
update help/docs to reflect the new env-only input method so the secret is no
longer exposed in process listings.
| Commands::Publish => { | ||
| let total_packages = planned.plan.packages.len(); | ||
| let mut progress = ProgressReporter::new(total_packages); | ||
|
|
||
| // Show initial progress if we have packages | ||
| if total_packages > 0 { | ||
| let first_pkg = &planned.plan.packages[0]; | ||
| progress.set_package(1, &first_pkg.name, &first_pkg.version); | ||
| } | ||
|
|
||
| let receipt = engine::run_publish(&planned, &opts, &mut reporter)?; | ||
|
|
||
| progress.finish(); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
ProgressReporter never receives intermediate updates during publishing.
The reporter is initialized and set to the first package, then engine::run_publish runs without access to the reporter, and finally progress.finish() is called. The progress bar will display the first package for the entire publish duration with no incremental updates. Either pass the reporter (or a callback) into the engine, or defer the progress integration until the engine supports it — the current code gives a misleading impression of progress.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 426 - 438, The ProgressReporter
is created and initialized (ProgressReporter::new and progress.set_package) but
engine::run_publish is invoked without it, so intermediate updates never occur;
modify engine::run_publish to accept a mutable ProgressReporter (or a
callback/trait object) and pass &mut progress (or a closure) from the Publish
branch so the engine can call progress.set_package / progress.increment during
publish, then call progress.finish() after run_publish returns; update any call
sites and the run_publish signature accordingly (or alternatively defer creating
progress until engine reports support) to ensure ProgressReporter receives
updates.
| // Rough time estimate (assuming ~30s per package + network overhead) | ||
| let total_packages = ws.plan.packages.len(); | ||
| let estimated_sequential_secs = total_packages * 30; | ||
| let estimated_parallel_secs = levels.iter().map(|_l| 30).sum::<usize>(); | ||
| println!( | ||
| " Estimated time (sequential): ~{}s ({:.1}min)", | ||
| estimated_sequential_secs, | ||
| estimated_sequential_secs as f64 / 60.0 | ||
| ); | ||
| println!( | ||
| " Estimated time (parallel): ~{}s ({:.1}min)", | ||
| estimated_parallel_secs, | ||
| estimated_parallel_secs as f64 / 60.0 | ||
| ); |
There was a problem hiding this comment.
Parallel time estimate ignores package count per level.
estimated_parallel_secs is always 30 * number_of_levels, regardless of how many packages are in each level. A level with 10 packages won't finish in 30s if max_concurrent is, say, 2. Consider factoring in level.packages.len() and any concurrency limit for a more realistic estimate:
// e.g., ceil(packages_in_level / max_concurrent) * 30This is labeled "rough estimate," but it could mislead users with large levels.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 694 - 707, The parallel estimate
currently uses 30 * number_of_levels and ignores how many packages are in each
level and any concurrency limit; update the computation of
estimated_parallel_secs to sum per-level durations using the package count and
max concurrency (for each level compute ceil(level.packages.len() /
max_concurrent) * 30 and sum them). Locate the block using variables levels and
estimated_parallel_secs in main.rs (the code that prints "Estimated time
(parallel)"), add or reference the existing max_concurrent value (or derive it
from the runner settings), and replace the simple levels.iter().map(|_l|
30).sum() with a per-level calculation that uses level.packages.len() and
max_concurrent (use integer-safe ceiling via (n + max_concurrent - 1) /
max_concurrent).
| println!(" - run:"); | ||
| println!(" name: Publish Crates"); | ||
| println!(" command: shipper publish"); | ||
| println!(" environment:"); | ||
| println!(" CARGO_REGISTRY_TOKEN: ${{{{ CARGO_REGISTRY_TOKEN }}}}"); |
There was a problem hiding this comment.
CircleCI snippet uses GitHub Actions ${{ }} syntax — env var will be a literal string, not the secret.
The output on line 1215 will be CARGO_REGISTRY_TOKEN: ${{ CARGO_REGISTRY_TOKEN }}, which is GitHub Actions expression syntax. CircleCI doesn't interpret this — it will set the var to the literal string. Since the snippet already uses context: cargo-registry (line 1232) to inject secrets, this environment: block is unnecessary and should be removed.
Proposed fix
println!(" - run:");
println!(" name: Publish Crates");
println!(" command: shipper publish");
- println!(" environment:");
- println!(" CARGO_REGISTRY_TOKEN: ${{{{ CARGO_REGISTRY_TOKEN }}}}");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| println!(" - run:"); | |
| println!(" name: Publish Crates"); | |
| println!(" command: shipper publish"); | |
| println!(" environment:"); | |
| println!(" CARGO_REGISTRY_TOKEN: ${{{{ CARGO_REGISTRY_TOKEN }}}}"); | |
| println!(" - run:"); | |
| println!(" name: Publish Crates"); | |
| println!(" command: shipper publish"); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-cli/src/main.rs` around lines 1211 - 1215, The CircleCI YAML
generator is printing a GitHub Actions-style expression which will set the env
var to a literal string; remove the unnecessary environment block output so
CircleCI uses the previously declared context: specifically delete or stop
emitting the println! that prints " CARGO_REGISTRY_TOKEN: ${{{{
CARGO_REGISTRY_TOKEN }}}}" (and the surrounding "environment:" lines printed via
println!) so the secret from the existing context: cargo-registry is used
instead.
| #[derive(Debug, Clone)] | ||
| pub struct RuntimeOptions { | ||
| pub allow_dirty: bool, | ||
| pub skip_ownership_check: bool, | ||
| pub strict_ownership: bool, | ||
| pub no_verify: bool, | ||
| pub max_attempts: u32, | ||
| pub base_delay: Duration, | ||
| pub max_delay: Duration, | ||
| /// Retry strategy type: immediate, exponential, linear, constant | ||
| pub retry_strategy: shipper_retry::RetryStrategyType, | ||
| /// Jitter factor for retry delays | ||
| pub retry_jitter: f64, | ||
| /// Per-error-type retry configuration | ||
| pub retry_per_error: shipper_retry::PerErrorConfig, | ||
| pub verify_timeout: Duration, | ||
| pub verify_poll_interval: Duration, | ||
| pub state_dir: PathBuf, | ||
| pub force_resume: bool, | ||
| pub policy: PublishPolicy, | ||
| pub verify_mode: VerifyMode, | ||
| pub readiness: ReadinessConfig, | ||
| pub output_lines: usize, | ||
| /// Force override of existing locks | ||
| pub force: bool, | ||
| /// Lock timeout duration (after which locks are considered stale) | ||
| pub lock_timeout: Duration, | ||
| /// Parallel publishing configuration | ||
| pub parallel: ParallelConfig, | ||
| /// Webhook configuration for publish notifications | ||
| pub webhook: WebhookConfig, | ||
| /// Encryption configuration for state files | ||
| pub encryption: EncryptionSettings, | ||
| /// Target registries for multi-registry publishing | ||
| pub registries: Vec<Registry>, | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
RuntimeOptions directly references shipper_retry types without a top-level use import.
Lines 560 and 564 use fully-qualified shipper_retry::RetryStrategyType and shipper_retry::PerErrorConfig while other cross-crate types (WebhookConfig, EncryptionSettings) are imported at the top. Consider adding a use shipper_retry::{...} at the top for consistency.
Proposed fix
Add at the top with the other imports:
use shipper_encrypt::EncryptionConfig as EncryptionSettings;
use shipper_webhook::WebhookConfig;
+use shipper_retry::{RetryStrategyType, PerErrorConfig};Then update the struct fields:
- pub retry_strategy: shipper_retry::RetryStrategyType,
+ pub retry_strategy: RetryStrategyType,
/// Jitter factor for retry delays
pub retry_jitter: f64,
/// Per-error-type retry configuration
- pub retry_per_error: shipper_retry::PerErrorConfig,
+ pub retry_per_error: PerErrorConfig,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/shipper-types/src/lib.rs` around lines 550 - 585, RuntimeOptions
currently references shipper_retry::RetryStrategyType and
shipper_retry::PerErrorConfig inline which is inconsistent with other
cross-crate types; add a top-level use statement like use
shipper_retry::{RetryStrategyType, PerErrorConfig}; then update the
RuntimeOptions struct to use the unqualified types RetryStrategyType and
PerErrorConfig (symbols: RuntimeOptions, RetryStrategyType, PerErrorConfig,
shipper_retry) to match how WebhookConfig/EncryptionSettings are imported.
|
Re: Copilot |
Summary
micro-*features or the umbrellamicro-allfeature.shipper-store,shipper-types, andshipper-webhookadded to the workspace.proptest-based property tests and concurrency stress tests for core data structures and the publishing engine.processmodule for better separation of concerns.#[cfg(test)]instead of blanket#[allow(dead_code)].CI Coverage
Existing CI already validates
micro-allcomprehensively — no workflow changes needed:cargo nextest run --workspace --all-featureson ubuntu/windows/macosmicro-*+micro-all+ default)cargo clippy --workspace --all-targets --all-featuresTest plan
--all-features