Skip to content

feat: micro backend modules, new crates, and expanded test coverage#4

Merged
EffortlessSteven merged 16 commits into
mainfrom
next-pr
Feb 22, 2026
Merged

feat: micro backend modules, new crates, and expanded test coverage#4
EffortlessSteven merged 16 commits into
mainfrom
next-pr

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

  • Micro backend modules: Introduces fine-grained feature-gated modules for auth, git, events, lock, encrypt, environment, storage, cargo, registry, process, webhook, types, config, state, and store — enabling consumers to pull in only what they need via individual micro-* features or the umbrella micro-all feature.
  • New crates: shipper-store, shipper-types, and shipper-webhook added to the workspace.
  • Property & stress tests: Adds proptest-based property tests and concurrency stress tests for core data structures and the publishing engine.
  • Preflight checks: New preflight validation tests covering git cleanliness, registry reachability, and dry-run scenarios.
  • Process module refactor: Extracts cargo command execution into a dedicated process module for better separation of concerns.
  • Clippy cleanup: Scopes test-only items under #[cfg(test)] instead of blanket #[allow(dead_code)].
  • HMAC webhook signatures: Updates Rust version requirement and adds HMAC support for webhook signature verification.

CI Coverage

Existing CI already validates micro-all comprehensively — no workflow changes needed:

  • cargo nextest run --workspace --all-features on ubuntu/windows/macos
  • BDD tests across 17 feature combinations (each micro-* + micro-all + default)
  • cargo clippy --workspace --all-targets --all-features
  • MSRV 1.92, fuzz smoke, cross-platform builds (5 targets)

Test plan

  • CI passes on all 3 platforms (ubuntu, windows, macos)
  • BDD matrix (17 feature combos) all green
  • Clippy clean under --all-features
  • MSRV 1.92 check passes
  • Fuzz smoke tests pass

…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.
… 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.
Copilot AI review requested due to automatic review settings February 22, 2026 10:36
@coderabbitai

coderabbitai Bot commented Feb 22, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Multi-registry publishing, configurable retry/backoff strategies, optional state-file encryption, webhook notifications, shell completion, and real-time publish progress reporting.
  • Documentation
    • Added CONTRIBUTING, SECURITY, ROADMAP, expanded README and release checklist.
  • Tests
    • Large suite of BDD and end-to-end tests covering publish, preflight, resume, CI templates, and property tests.
  • Chores
    • CI workflows (lint, tests, coverage, fuzz, cross-platform, release) and Dependabot configured for weekly updates.

Walkthrough

Adds 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

Cohort / File(s) Summary
CI & Automation
​.config/nextest.toml, .github/dependabot.yml, .github/workflows/ci.yml, .github/workflows/fuzz.yml, .github/workflows/release.yml
Add Nextest profiles, Dependabot cargo/Actions grouping, and multi-job GitHub Actions (lint, tests/nextest, MSRV, security/audit, docs, coverage, BDD, fuzz, cross-platform, release) with caching and artifact uploads.
Docs & governance
CONTRIBUTING.md, README.md, ROADMAP.md, SECURITY.md, RELEASE_CHECKLIST_v0.2.0.md
New contributor guide, roadmap, security policy, release checklist updates, and workspace/CLI documentation.
Workspace manifest
Cargo.toml
Expands workspace members (many new crates), sets workspace metadata (version 0.2.0, edition, license, rust-version), resolver = "3", and excludes fuzz.
Core types & model
crates/shipper-types/...
Introduce comprehensive domain model: registry, release/plan, package state/progress, receipts, events, readiness/readme, serialization helpers, and tests.
Auth & token resolution
crates/shipper-auth/..., crates/shipper/src/auth*.rs
New shipper-auth crate and auth_micro integration: resolve tokens from env/credentials.toml, detect auth type (Token/Trusted/Unknown/None), and expose AuthInfo/TokenSource plus tests.
Encryption & state encryption
crates/shipper-encrypt/..., crates/shipper/src/encryption*.rs
AES‑256‑GCM + PBKDF2 utilities, EncryptionConfig, StateEncryption wrapper, file read/write helpers, and encrypted state APIs/tests.
Cargo, process & execution helpers
crates/shipper-cargo/..., crates/shipper-process/..., crates/shipper/src/process*.rs, crates/shipper/src/cargo*.rs
Workspace cargo metadata loader, cargo publish/dry-run helpers with redaction, and robust command execution with timeout support and unified output structs.
Configuration & runtime options
crates/shipper-config/..., crates/shipper/src/config*.rs
Typed ShipperConfig, MultiRegistry support, Retry/Policy/Output/Lock/Encryption/Storage sections, build_runtime_options merging CLI overrides, validation, and TOML template/tests.
State, receipts & store
crates/shipper-state/..., crates/shipper-store/..., crates/shipper-events/...
State/receipt file management with versioning/migrations, encrypted load/save variants, append‑only JSONL EventLog, and FileStore/StateStore abstractions.
Registry & storage backends
crates/shipper-registry/..., crates/shipper-storage/...
Blocking registry client (crate/version/owners/index checks, sparse index helpers) and StorageBackend trait with filesystem implementation and CloudStorageConfig wiring.
Retry, webhook & locking
crates/shipper-retry/..., crates/shipper-webhook/..., crates/shipper-lock/...
Retry framework (strategies, jitter, per-error), webhook client (Generic/Slack/Discord with HMAC signing), and file-based LockFile with stale-lock handling.
CLI & UX
crates/shipper-cli/...
CLI: new global flags (retry/webhook/encrypt/registries), shell completion command, TTY-aware ProgressReporter, CI template generators, BDD and e2e tests, and README.
Engine & publish flows
crates/shipper/src/engine*.rs, crates/shipper/src/engine_parallel.rs
New preflight/publish/resume flows, Reporter extended (info/warn/error), webhook emissions, readiness checks, retry/backoff integration, event recording, state persistence, and parallel engine webhook integration.
Feature gating & micro-modules
crates/shipper/src/lib.rs, many *_micro.rs files
Introduce feature-gated micro-* re-exports (auth, cargo, process, config, etc.) to allow modular consumption or full monolith via feature flags.
Tests & proptests
crates/shipper/.../property_tests, crates/shipper-cli/tests/*, crates/shipper/proptest-regressions/*
Extensive unit, integration, property, and BDD/E2E tests covering topology, state transitions, CLI flows, mock registry, and proptest regression seeds.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 Sixteen crates sprout in the burrow tonight,
Tokens and locks snug under moonlight,
Preflight hops, then publish takes flight,
Webhooks cheer as packages alight,
A small rabbit applauds—modular delight! 🌿

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: introduction of micro backend modules, new crates, and expanded test coverage.
Description check ✅ Passed The PR description is comprehensive and clearly related to the changeset, detailing micro backend modules, new crates, test additions, refactoring, and CI coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch next-pr

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @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

  • Micro-backend Modules: Introduced fine-grained, feature-gated modules for various functionalities like authentication, Git operations, event logging, and more, allowing consumers to selectively include necessary features.
  • New Crates: Added shipper-store, shipper-types, and shipper-webhook to the workspace, enhancing modularity and separation of concerns.
  • Expanded Test Coverage: Integrated proptest-based property tests and concurrency stress tests for core data structures and the publishing engine, alongside new BDD tests.
  • Enhanced Preflight Checks: Implemented new preflight validation tests covering Git cleanliness, registry reachability, and dry-run scenarios to improve reliability.
  • Process Module Refactor: Extracted Cargo command execution into a dedicated process module for better separation of concerns and maintainability.
  • Clippy Cleanup: Improved code quality by scoping test-only items under #[cfg(test)] instead of using blanket #[allow(dead_code)].
  • HMAC Webhook Signatures: Updated Rust version requirements and added HMAC support for webhook signature verification, enhancing security.
Changelog
  • .config/nextest.toml
    • Added Nextest configuration for CI, stress, and nightly profiles.
  • .github/dependabot.yml
    • Added Dependabot configuration for Rust and GitHub Actions dependencies, including grouping for minor/patch updates.
  • CONTRIBUTING.md
    • Added a comprehensive contributing guide covering setup, making changes, testing, and PR process.
  • Cargo.lock
    • Updated dependencies to reflect the addition of new micro-backend crates and their transitive dependencies.
  • Cargo.toml
    • Added new shipper-* crates to the workspace members.
    • Defined feature flags for micro-backend modules, enabling granular dependency management.
  • README.md
    • Updated README to include a new 'Workspace crates' section.
    • Added details on shipper doctor authentication types.
  • RELEASE_CHECKLIST_v0.2.0.md
    • Updated test counts to reflect increased coverage from new features and tests.
  • ROADMAP.md
    • Added a new roadmap document outlining future development, design principles, and feature specifications.
  • SECURITY.md
    • Added a new security policy document detailing supported versions, vulnerability reporting, and security considerations.
  • crates/shipper-auth/Cargo.toml
    • Added new crate shipper-auth for authentication and token resolution.
  • crates/shipper-auth/src/lib.rs
    • Implemented authentication logic for resolving registry tokens from environment variables and Cargo credentials files.
  • crates/shipper-cargo/Cargo.toml
    • Added new crate shipper-cargo for Cargo workspace metadata and command execution.
  • crates/shipper-cargo/src/lib.rs
    • Implemented utilities for loading Cargo metadata, executing Cargo commands, and redacting sensitive output.
  • crates/shipper-cli/Cargo.toml
    • Updated dependencies to include new micro-backend crates.
    • Added feature flags to enable/disable micro-backend modules.
    • Added new CLI dependencies for progress bars and shell completion.
  • crates/shipper-cli/README.md
    • Added a dedicated README for shipper-cli outlining installation, quick start, core commands, and configuration.
  • crates/shipper-cli/src/main.rs
    • Updated CLI argument parsing to support new global flags and subcommands.
    • Integrated progress reporting for publish and resume commands.
    • Added a completion command for generating shell completion scripts.
    • Enhanced plan command output with detailed dependency analysis.
    • Updated doctor command to detect trusted publishing authentication types.
    • Modified clean command to support force removal of lock files.
  • crates/shipper-cli/src/progress.rs
    • Added a new module for CLI progress reporting with TTY detection and fallback.
  • crates/shipper-cli/tests/bdd_publish.rs
    • Added new BDD tests to verify publish workflow behaviors, including dependency order, preflight checks, and resumability.
  • crates/shipper-cli/tests/cli_e2e.rs
    • Updated snapshots for plan and doctor commands.
    • Added new test cases for preflight event logging and trusted publishing detection.
  • crates/shipper-config/Cargo.toml
    • Added new crate shipper-config for configuration file handling.
  • crates/shipper-config/src/lib.rs
    • Implemented configuration structure, validation, and logic for merging CLI overrides with TOML file settings.
  • crates/shipper-encrypt/Cargo.toml
    • Added new crate shipper-encrypt for state file encryption.
  • crates/shipper-encrypt/src/lib.rs
    • Implemented AES-256-GCM encryption and decryption logic with PBKDF2 key derivation.
  • crates/shipper-environment/Cargo.toml
    • Added new crate shipper-environment for environment fingerprinting.
  • crates/shipper-environment/src/lib.rs
    • Implemented environment detection (CI, OS, architecture) and fingerprinting capabilities.
  • crates/shipper-events/Cargo.toml
    • Added new crate shipper-events for event logging.
  • crates/shipper-events/src/lib.rs
    • Implemented an append-only JSONL event log for tracking publish operations.
  • crates/shipper-git/Cargo.toml
    • Added new crate shipper-git for Git operations.
  • crates/shipper-git/src/lib.rs
    • Implemented functions for checking Git cleanliness, retrieving commit info, and managing Git context.
  • crates/shipper-lock/Cargo.toml
    • Added new crate shipper-lock for file-based locking.
  • crates/shipper-lock/src/lib.rs
    • Implemented a file-based locking mechanism to prevent concurrent operations, including stale lock detection.
  • crates/shipper-process/Cargo.toml
    • Added new crate shipper-process for process execution.
  • crates/shipper-process/src/lib.rs
    • Implemented utilities for running external commands with timeout support and output capture.
  • crates/shipper-registry/Cargo.toml
    • Added new crate shipper-registry for registry API client functionality.
  • crates/shipper-registry/src/lib.rs
    • Implemented a client for interacting with crate registries, including version existence and owner checks.
  • crates/shipper-retry/Cargo.toml
    • Added new crate shipper-retry for retry strategies and backoff policies.
  • crates/shipper-retry/src/lib.rs
    • Implemented configurable retry strategies with exponential, linear, and constant backoff, plus jitter.
  • crates/shipper-state/Cargo.toml
    • Added new crate shipper-state for state persistence.
  • crates/shipper-state/src/lib.rs
    • Implemented state and receipt persistence with schema versioning and migration support.
  • crates/shipper-storage/Cargo.toml
    • Added new crate shipper-storage for storage backends.
  • crates/shipper-storage/src/lib.rs
    • Implemented a StorageBackend trait and a FileStorage backend for local file system persistence.
  • crates/shipper-store/Cargo.toml
    • Added new crate shipper-store for state store abstraction.
  • crates/shipper-store/src/lib.rs
    • Implemented a StateStore trait and a FileStore for managing execution state, receipts, and event logs.
  • crates/shipper-types/Cargo.toml
    • Added new crate shipper-types for core domain types.
  • crates/shipper-types/src/lib.rs
    • Defined core domain types for Shipper, including ReleaseSpec, ReleasePlan, RuntimeOptions, and Receipt.
  • crates/shipper-webhook/Cargo.toml
    • Added new crate shipper-webhook for webhook notifications.
  • crates/shipper-webhook/src/lib.rs
    • Implemented webhook notification functionality for publish events, including HMAC signature support.
  • crates/shipper/proptest-regressions/property_tests.txt
    • Added regression seeds for property tests to ensure past failure cases are re-tested.
  • crates/shipper/src/auth.rs
    • Modified the monolithic auth module to delegate to the shipper-auth microcrate when its feature is enabled.
    • Updated detect_auth_type to use the new AuthType enum.
  • crates/shipper/src/auth_micro.rs
    • Added a new shim file for the shipper-auth microcrate, maintaining API compatibility.
  • crates/shipper/src/cargo.rs
    • Refactored Cargo command execution to utilize the new shipper-process microcrate for timeout handling.
  • crates/shipper/src/cargo_micro.rs
    • Added a new shim file for the shipper-cargo microcrate.
  • crates/shipper/src/config.rs
    • Modified the monolithic config module to delegate to the shipper-config microcrate when its feature is enabled.
    • Integrated new configuration options for webhooks, encryption, and multi-registry publishing.
  • crates/shipper/src/config_micro.rs
    • Added a new shim file for the shipper-config microcrate.
  • crates/shipper/src/encryption.rs
    • Added a new module for state file encryption logic, utilizing AES-256-GCM.
  • crates/shipper/src/encryption_micro.rs
    • Added a new shim file for the shipper-encrypt microcrate.
  • crates/shipper/src/engine.rs
    • Integrated webhook notifications for publish lifecycle events (started, succeeded, failed, completed).
    • Enhanced preflight event logging to capture more detailed verification steps.
    • Updated backoff delay calculation to use configurable retry strategies and jitter.
  • crates/shipper/src/engine_parallel.rs
    • Integrated webhook notifications for parallel publish events.
    • Updated backoff delay calculation to use configurable retry strategies and jitter.
  • crates/shipper/src/environment_micro.rs
    • Added a new shim file for the shipper-environment microcrate.
  • crates/shipper/src/events_micro.rs
    • Added a new shim file for the shipper-events microcrate.
  • crates/shipper/src/git.rs
    • Removed 2>/dev/null from git describe command in tests for better cross-platform compatibility.
  • crates/shipper/src/git_micro.rs
    • Added a new shim file for the shipper-git microcrate.
  • crates/shipper/src/lib.rs
    • Refactored the main library to introduce feature-gated micro-backend modules for modularity.
    • Updated module imports and documentation to reflect the new micro-backend architecture.
  • crates/shipper/src/lock_micro.rs
    • Added a new shim file for the shipper-lock microcrate.
  • crates/shipper/src/plan.rs
    • Modified load_metadata to use the shipper-cargo microcrate when its feature is enabled.
    • Updated ReleasePlan::group_by_levels to use crate::types::PublishLevel.
  • crates/shipper/src/process.rs
    • Added a new module for generic process execution with timeout capabilities.
  • crates/shipper/src/process_micro.rs
    • Added a new shim file for the shipper-process microcrate.
  • crates/shipper/src/property_tests.rs
    • Added a new module for property-based tests to verify core invariants.
  • crates/shipper/src/registry_micro.rs
    • Added a new shim file for the shipper-registry microcrate.
  • crates/shipper/src/state_micro.rs
    • Added a new shim file for the shipper-state microcrate.
  • crates/shipper/src/storage_micro.rs
    • Added a new shim file for the shipper-storage microcrate.
  • crates/shipper/src/store_micro.rs
    • Added a new shim file for the shipper-store microcrate.
  • crates/shipper/src/stress_tests.rs
    • Added a new module for concurrency stress tests.
  • crates/shipper/src/types_micro.rs
    • Added a new shim file for the shipper-types microcrate.
  • crates/shipper/src/webhook_micro.rs
    • Added a new shim file for the shipper-webhook microcrate.
Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/ci.yml
    • .github/workflows/fuzz.yml
    • .github/workflows/release.yml
Activity
  • The pull request description indicates that existing CI comprehensively validates the micro-all feature, requiring no workflow changes.
  • The author has outlined a test plan, including passing CI on all three platforms (Ubuntu, Windows, macOS), a BDD matrix across 17 feature combinations, clean Clippy checks, MSRV 1.92 verification, and fuzz smoke tests.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 proptest and 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

Comment on lines +8 to +10
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct CommandOutput {

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#[allow(dead_code)]

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct CommandOutput {

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17
#[allow(dead_code)]
pub(crate) fn run_command_with_timeout(

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
.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);

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread crates/shipper/src/git.rs
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
use std::process::Command;

Copilot uses AI. Check for mistakes.

/// 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)]

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#[allow(dead_code)]
#[cfg(test)]

Copilot uses AI. Check for mistakes.

/// Marks the current package as completed.
#[allow(clippy::collapsible_if)]
#[allow(dead_code)]

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#[allow(dead_code)]
#[cfg(test)]

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +120
#[allow(dead_code)]
pub fn set_status(&self, status: &str) {

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function has #[allow(dead_code)] attribute. According to the coding guidelines, test-only items should use #[cfg(test)] instead of blanket dead code allowances.

Copilot uses AI. Check for mistakes.
license.workspace = true
rust-version.workspace = true
description = "Retry strategies and backoff policies for shipper"
repository = "https://github.com/cmrigney/shipper"

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repository URL uses different GitHub username (cmrigney) compared to other microcrates which use EffortlessMetrics. This inconsistency should be corrected for uniformity across the workspace.

Suggested change
repository = "https://github.com/cmrigney/shipper"
repository = "https://github.com/EffortlessMetrics/shipper"

Copilot uses AI. Check for mistakes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if cli.registry.is_none() {
planned.plan.registry.name = reg_config.name.clone();
}

P1 Badge Build the plan with the effective registry

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".

Comment thread crates/shipper/Cargo.toml

[dependencies]
# Workspace microcrates
shipper-retry = { path = "../shipper-retry" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@codecov

codecov Bot commented Feb 22, 2026

Copy link
Copy Markdown

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 ☂️

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +574 to +583
// 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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Enforcing that only one registry can be marked as default is crucial. This prevents ambiguity and ensures predictable behavior when resolving the default registry for publishing operations.

}
}
eprintln!("[warn] Use --force to override the lock");
bail!("cannot clean: active lock exists");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message for an active lock could be more actionable. Suggesting --force or waiting for expiration would improve user experience.

Suggested change
bail!("cannot clean: active lock exists");
bail!("cannot clean: active lock exists. Use --force to override or wait for the lock to expire.");

Comment on lines +522 to +524
// Validate jitter
if self.retry.jitter < 0.0 || self.retry.jitter > 1.0 {
bail!("retry.jitter must be between 0.0 and 1.0");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Validating that retry.jitter is within the valid range of 0.0 to 1.0 prevents invalid configurations and maintains the integrity of the retry mechanism.

Comment on lines +564 to +572
// 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");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Validating that each configured registry has a name and API base prevents incomplete or invalid multi-registry configurations, ensuring operational reliability.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Publish 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 a documentation = "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-types before 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 | 🔵 Trivial

Tests use plain assertions instead of insta snapshot testing.

The coding guidelines recommend using insta for snapshot testing. While the current assertions are correct, consider snapshot-testing the TOML template output and the RuntimeOptions produced by build_runtime_options to improve regression detection as the config surface grows.

As per coding guidelines, "Use snapshot testing with insta for 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 | 🔵 Trivial

Redundant 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_registry server thread can hang if a test exits early.

The server thread blocks on server.recv() for exactly expected_requests iterations. 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 a try_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 | 🔵 Trivial

Minor: CargoOutput.duration is measured independently of CommandOutput.duration.

start.elapsed() at line 137 captures wall-clock time from the caller's perspective, while process::run_command_with_timeout also records its own duration internally (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 using output.duration instead.

🤖 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_levels is duplicated between plan.rs and shipper-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 | 🟠 Major

Unnecessary double-locking: SendReporter fields are Mutex<Vec<String>> but the struct is already behind Arc<Mutex<…>>.

The outer Mutex grants exclusive &mut self access. The inner Mutex on 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 | 🟡 Minor

ProgressReporter updates are incomplete—only the first package is displayed, with no progress tracking during the publish loop.

ProgressReporter is created and set_package() is called once for the first package before engine::run_publish() executes. However, engine::run_publish() receives the CliReporter (which implements the Reporter trait), not ProgressReporter. The progress object is then abandoned during the publish loop and finish() is called after completion. The finish_package() method exists but is never invoked in production code—it appears only in unit tests.

To properly track progress, either:

  • Pass progress to the engine and call progress.set_package() and progress.finish_package() for each package in the publish loop, or
  • Implement Reporter on ProgressReporter and use it instead of CliReporter
🤖 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 | 🟡 Minor

Add #[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 from serial_test for 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 | 🟠 Major

Record success when the registry confirms publish after a cargo failure.

When cargo publish fails but version_exists returns true, the package is marked Published without a PackagePublished event or PublishSucceeded webhook, 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.2

Apply 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.

Comment on lines +104 to +108
publish-crates-io:
name: Publish to crates.io
runs-on: ubuntu-latest
needs: create-release
environment: release

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +104 to +133
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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
done

Repository: 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
done

Repository: 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 -30

Repository: EffortlessMetrics/shipper

Length of output: 624


🏁 Script executed:

#!/bin/bash
# Search for publish field in all Cargo.toml files
rg "^publish\s*=" crates/ --type toml

Repository: 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 -5

Repository: 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.

Comment on lines +125 to +130
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
- 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.

Comment on lines +129 to +130
- name: Wait for index update
run: sleep 30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +36 to +42
/// 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment thread README.md
Comment on lines +15 to +18
## 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"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.

Comment thread README.md
Comment on lines +17 to +18
- [`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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
- [`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.

Comment thread ROADMAP.md
|-----------|---------|
| **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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
| **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.

Comment thread ROADMAP.md
| 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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: crate.iocrates.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.

Suggested change
| 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment thread .config/nextest.toml
Comment on lines +4 to +6
[profile.ci]
# Fail fast on CI to save time
fail-fast = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
[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.

Comment thread .config/nextest.toml
Comment on lines +28 to +31
[profile.nightly]
# Nightly profile for extended testing
retries = { count = 3, backoff = "exponential", delay = "2s" }
test-threads = "num-cpus" No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread .github/workflows/ci.yml
Comment on lines +114 to +115
- name: Check with MSRV
run: cargo check --workspace

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment thread .github/workflows/ci.yml
Comment on lines +230 to +233
fuzz-smoke:
name: Fuzz Smoke (PR)
if: github.event_name != 'schedule'
runs-on: ubuntu-latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +17 to +20
targets:
description: 'Specific targets to run (comma-separated, or "all")'
required: false
default: 'all'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +40 to +54
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,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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).

Comment on lines +18 to +34
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
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.

Comment thread SECURITY.md
Comment on lines +60 to +64
### Token Sources (in priority order)

1. `CARGO_REGISTRY_TOKEN` environment variable
2. `CARGO_TOKEN` environment variable (fallback)
3. Cargo credential store (`cargo login` configuration)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -10

Repository: 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.rs

Repository: 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 -i

Repository: 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.md

Repository: 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.rs

Repository: 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:

  1. CARGO_REGISTRY_TOKEN environment variable
  2. CARGO_REGISTRIES_<NAME>_TOKEN environment variable
  3. $CARGO_HOME/credentials.toml file

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.

Comment thread SECURITY.md
Comment on lines +89 to +95
### 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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.rs

Repository: 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/*.rs

Repository: 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.

Comment thread SECURITY.md
Comment on lines +100 to +118
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
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +79 to +129
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +416 to +485
/// 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.rs

Repository: 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.rs

Repository: 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 -100

Repository: 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 10

Repository: 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 5

Repository: 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 -30

Repository: 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 2

Repository: 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().

Comment on lines +151 to +157
/// 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>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1276 to +1277
fn run_clean(
state_dir: &PathBuf,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
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.

Comment on lines +8 to +14
use atty::Stream;
use indicatif::{ProgressBar, ProgressStyle};

/// Detects whether stdout is connected to a TTY.
pub fn is_tty() -> bool {
atty::is(Stream::Stdout)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

fd -t f "Cargo.toml" | head -5

Repository: EffortlessMetrics/shipper

Length of output: 198


🏁 Script executed:

cat Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 898


🏁 Script executed:

rg -l "atty" --type rs

Repository: EffortlessMetrics/shipper

Length of output: 95


🌐 Web query:

atty crate deprecated std::io::IsTerminal Rust 1.70 stabilization

💡 Result:

  • The atty crate 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::IsTerminal with the method is_terminal() as the standard replacement for what crates like atty provided. [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-terminal crate; its docs explicitly say most users should prefer std::io::IsTerminal as 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-list

Repository: EffortlessMetrics/shipper

Length of output: 458


🏁 Script executed:

rg "atty"

Repository: EffortlessMetrics/shipper

Length of output: 455


🏁 Script executed:

cat crates/shipper-cli/Cargo.toml

Repository: 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.

Suggested change
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.

Comment on lines +293 to +303
// 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(),
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +752 to +795
// 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(),
},
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +28 to +38
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}"))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +82 to +100
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)
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: 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 -20

Repository: 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 -120

Repository: 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 -10

Repository: 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 -200

Repository: 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 -30

Repository: 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 -50

Repository: 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.

Comment on lines +132 to +194
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.duration is discarded; start.elapsed() double-counts outer overhead.

process::run_command_with_timeout captures its own start and returns output.duration for the process execution time. cargo_publish (and the two dry-run helpers at lines 154 and 194) take a second Instant::now() before building args and then use start.elapsed() instead of output.duration. The outer duration includes a few microseconds of arg-building overhead and is slightly less precise for profiling. Consider using output.duration directly:

-    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) and cargo_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.

Comment thread .config/nextest.toml
Comment on lines +14 to +15
# Timeout for individual tests
test-threads = "num-cpus"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
# 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.

Comment thread .github/workflows/ci.yml
Comment on lines +84 to +85
- name: Run doc tests
run: cargo test --workspace --doc

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread .github/workflows/ci.yml
Comment on lines +193 to +198
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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_TOKEN secret. [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):
    • filedeprecated in favor of files
    • plugindeprecated in favor of plugins
    • 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 (requires permissions: 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.

Comment thread .github/workflows/ci.yml
Comment on lines +329 to +337
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check the workflow file to see lines 304 and 329-337
git ls-files .github/workflows/ci.yml

Repository: EffortlessMetrics/shipper

Length of output: 93


🏁 Script executed:

# Read the CI workflow file around the relevant lines
wc -l .github/workflows/ci.yml

Repository: EffortlessMetrics/shipper

Length of output: 97


🏁 Script executed:

# Check root Cargo.toml for [profile.release] and binary name
cat -n Cargo.toml | head -50

Repository: 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.yml

Repository: 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.toml

Repository: 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 -5

Repository: 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 2

Repository: 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 toml

Repository: 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.toml

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check if there's a [package] section in the root Cargo.toml
grep '\[package\]' Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check the entire root Cargo.toml structure
tail -20 Cargo.toml

Repository: 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 -30

Repository: 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.toml

Repository: 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.toml

Repository: 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 -20

Repository: 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")
EOF

Repository: 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 -5

Repository: 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.yml

Repository: 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.yml

Repository: 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).

Comment on lines +53 to +154
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,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +143 to +149
/// 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>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

--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.

Comment on lines 426 to +438
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +694 to +707
// 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
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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) * 30

This 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).

Comment on lines +1211 to +1215
println!(" - run:");
println!(" name: Publish Crates");
println!(" command: shipper publish");
println!(" environment:");
println!(" CARGO_REGISTRY_TOKEN: ${{{{ CARGO_REGISTRY_TOKEN }}}}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +550 to +585
#[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>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

@EffortlessSteven

Copy link
Copy Markdown
Member Author

Re: Copilot #[allow(dead_code)] on process.rs / process_micro.rs — feature-dead by design under the feature matrix; not test-only; clippy clean under --all-features.

@EffortlessSteven EffortlessSteven merged commit 86c9cf2 into main Feb 22, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants