Skip to content

Implement cargo publish with dry-run by default and --live option - #44

Merged
leynos merged 13 commits into
mainfrom
terragon/implement-lading-publish-command-tqvzvs
Dec 4, 2025
Merged

Implement cargo publish with dry-run by default and --live option#44
leynos merged 13 commits into
mainfrom
terragon/implement-lading-publish-command-tqvzvs

Conversation

@leynos

@leynos leynos commented Dec 2, 2025

Copy link
Copy Markdown
Owner

Summary

  • Introduces dry-run by default for cargo publish and a new --live flag to perform real publishing.
  • Crate publishing continues past already-published versions by logging a warning and proceeding to the next crate.
  • Integrates publish step into Lading workflow after packaging/preflight checks.
  • Updates tests and docs to reflect the new publish behavior.

Changes

Core functionality

  • Add PublishOptions.live: bool to control live vs. dry-run publishing.
  • Introduce _ALREADY_PUBLISHED_MARKERS and _is_already_published_error to detect common "already exists" errors from cargo publish.
  • Implement _publish_crates(plan, preparation, runner, live) to publish crates in order:
    • Use cargo publish --dry-run by default; omit --dry-run when live is True.
    • If cargo publish exits non-zero with an "already published/exists" message, log a warning and continue.
    • On other failures, raise PublishPreflightError with detailed message.
  • Wire _publish_crates into run() so publishing executes after preflight and packaging.

CLI

  • lading/cli.py: Add --live flag and propagate to PublishOptions.live.

Tests

  • tests/bdd/features/cli.feature: Added scenarios for
    • publishing in dry-run mode
    • publishing in live mode
    • continuing when a crate is already uploaded
  • tests/bdd/steps/test_publish_steps.py:
    • Adjusted typing to support ResponseProvider for preflight overrides.
    • Implemented CLI path for --live invocation and verification of dry-run/live behavior.
    • Added scenario to simulate cargo publish reporting a crate already uploaded.
    • Added helper to fetch publish invocations from the preflight recorder for assertions.
  • tests/unit/publish/test_packaging.py:
    • Added unit tests for:
      • dry-run publish order across crates
      • live publish mode omitting --dry-run
      • continuing when a version is already uploaded
      • failing cargo publish aborts workflow with detailed error

Documentation

  • docs/lading-design.md: Update publish section to describe:
    • default dry-run behavior
    • --live mode to perform real cargo publish
    • behavior when a crate version already exists (log warning, continue)
  • docs/roadmap.md: Mark the cargo publish execution as completed.
  • docs/usage-guide.md: Document usage of --live and explain dry-run vs live publishing workflow.

Design & Behavior

  • Default dry-run ensures operators can validate the full pipeline without uploading crates.
  • When --live is supplied, cargo publish runs without --dry-run for real uploads.
  • If cargo publish reports a crate version already exists, Lading logs a warning and continues to the next crate instead of aborting, providing resilience in multi-crate workflows.
  • After packaging, Lading normalizes manifests as before and then proceeds to publish in plan order.

Testing plan

  • Unit tests for dry-run vs live publishing behavior and error handling.
  • Integration/BDD tests to verify:
    • dry-run publish invocations for crates in order
    • live publish invocations without --dry-run
    • handling of already-published crates without failing the run
  • Documentation reflects new defaults and usage.

How to test manually

  • Dry-run (default):
    • python -m lading.cli --workspace-root /path/to/workspace publish
    • Observe cargo publish --dry-run invocations for each publishable crate in order
  • Live publish: pass --live:
    • python -m lading.cli --workspace-root /path/to/workspace publish --live
    • Observe cargo publish invocations omitting --dry-run and crates are actually uploaded

Compatibility

  • This change preserves existing behavior by default (dry-run) and adds an opt-in live mode.
  • Behavior when a crate already exists is now tolerant, logging a warning and continuing, rather than aborting the entire publish loop.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

📎 Task: https://www.terragonlabs.com/task/05f608c8-11dc-4ca3-b5b0-ccfb3a41a386

Summary by Sourcery

Implement default dry-run cargo publishing with an opt-in live mode and integrate crate publishing into the existing publish workflow after preflight checks and packaging.

New Features:

  • Add support for executing cargo publish as part of the publish workflow, defaulting to dry-run with an optional --live flag to perform real uploads.

Enhancements:

  • Introduce structured handling of cargo publish failures, including tolerant behaviour for already-published crate versions and consistent error messaging.
  • Extend logging around the publish phase to surface dry-run/live execution details and already-published warnings.
  • Refactor publish packaging tests to share fixtures and improve coverage of error reporting and publish behaviour.

Documentation:

  • Update design and usage documentation to describe the new publish pipeline, default dry-run behaviour, and --live option, and mark cargo publish execution as complete in the roadmap.

Tests:

  • Add BDD and unit tests to cover dry-run vs live cargo publish, continuation past already-published crate versions, and detailed failure reporting.
  • Restructure publish BDD step definitions into dedicated helper, fixture, and step modules for clearer test organisation.

Chores:

  • Register new publish-related step modules in the global test configuration.

…lished crates

- Extend the publish command to support a --live flag to run `cargo publish` without `--dry-run`.
- Default to dry-run mode unless explicitly enabled live.
- Log a warning and continue publishing other crates if a crate version is already published instead of aborting.
- Update CLI interface, internal publish logic, documentation, and tests to support live publishing and error handling.
- Add BDD scenarios and unit tests covering live publishing and publishing already uploaded crates.

This enables safely executing real crate publications while improving robustness against already published versions.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Dec 2, 2025

Copy link
Copy Markdown

Reviewer's Guide

Implements an integrated cargo publish phase that runs after packaging, defaults to dry-run, supports an opt-in --live mode, and handles already-published crate versions gracefully, with supporting CLI, test, and documentation updates.

Sequence diagram for updated publish workflow with dry-run and live cargo publish

sequenceDiagram
    actor Operator
    participant CLI as LadingCLI
    participant Publish as PublishCommand
    participant Cargo as CargoToolchain

    Operator->>CLI: publish [--live?]
    CLI->>Publish: run(workspace_root, PublishOptions(live))

    rect rgb(230,230,230)
        Note over Publish: Pre-flight and packaging
        Publish->>Publish: _ensure_configuration(...)
        Publish->>Publish: _build_preflight_environment(...)
        Publish->>Cargo: cargo package (per crate)
        Cargo-->>Publish: exit_code, stdout, stderr
        Publish->>Publish: _format_cargo_failure_message("package", ...)
        Publish-->>Operator: PublishPreflightError on failure
    end

    rect rgb(220,245,220)
        Note over Publish: New publish phase
        loop for each crate in plan.publishable
            Publish->>Publish: _resolve_staged_crate_root(...)
            alt live == False
                Publish->>Cargo: cargo publish --dry-run
            else live == True
                Publish->>Cargo: cargo publish
            end
            Cargo-->>Publish: exit_code, stdout, stderr

            alt exit_code == 0
                Publish->>Publish: continue to next crate
            else exit_code != 0
                Publish->>Publish: _is_already_published_error(exit_code, stdout, stderr)
                alt already published
                    Publish->>Publish: LOGGER.warning("already published - skipping")
                    Publish->>Publish: continue to next crate
                else other failure
                    Publish->>Publish: _format_cargo_failure_message("publish", ...)
                    Publish-->>Operator: PublishError
                end
            end
        end
    end

    Publish-->>Operator: plan_message (publish summary)
Loading

Class diagram for updated publish options and errors

classDiagram
    class PublishOptions {
        bool allow_dirty = True
        bool live = False
        Path build_directory
        bool preserve_symlinks = True
        bool cleanup = False
        bool dry_run
        LadingConfig configuration
    }

    class PublishPreflightError {
        <<exception>>
    }

    class PublishError {
        <<exception>>
    }

    PublishPreflightError <|-- PublishError

    class PublishModuleHelpers {
        +_format_cargo_failure_message(command, crate_name, exit_code, output) str
        +_is_already_published_error(exit_code, stdout, stderr) bool
        +_publish_crates(plan, preparation, runner, live) void
    }

    class PublishPlan {
        +publishable
    }

    class PublishPreparation {
        +staging_root
    }

    PublishModuleHelpers --> PublishOptions : uses
    PublishModuleHelpers --> PublishPlan : iterates publishable
    PublishModuleHelpers --> PublishPreparation : reads staging_root
    PublishError --> PublishModuleHelpers : raised by
    PublishPreflightError --> PublishModuleHelpers : raised by
Loading

File-Level Changes

Change Details Files
Introduce live/dry-run publishing options and error typing in the publish command implementation.
  • Extend PublishOptions with a live boolean flag controlling dry-run vs live cargo publish
  • Add a PublishError subclass of PublishPreflightError for publish-phase failures
  • Refactor cargo failure message construction into a reusable _format_cargo_failure_message helper
lading/commands/publish.py
Implement cargo publish execution with dry-run by default, tolerant handling of already-published versions, and integration into the publish workflow.
  • Add _ALREADY_PUBLISHED_MARKERS and registry error code constant for detecting already-published errors from cargo publish output
  • Implement _is_already_published_error to recognise registry 'already exists' failures based on exit code and stderr/stdout markers
  • Implement _publish_crates to run cargo publish per crate in plan order, use --dry-run unless live is True, log info/warnings, skip already-published versions, and raise PublishError on other failures
  • Invoke _publish_crates from run() after _package_publishable_crates to complete the workflow
lading/commands/publish.py
tests/unit/publish/test_packaging.py
Refactor packaging tests and add unit coverage for publish dry-run/live behaviour and failure handling.
  • Introduce a publish_plan_and_prep fixture to share plan/preparation setup across tests
  • Add CallTrackingRunner and make_failing_runner helpers to record/drive command invocations in tests
  • Use _format_cargo_failure_message via new helpers to assert stderr-preferred error messages over stdout for packaging failures
  • Add unit tests verifying cargo publish dry-run order, live mode omitting --dry-run, continuing on already-uploaded versions, and raising PublishPreflightError on other failures
tests/unit/publish/test_packaging.py
Add CLI support for a --live flag and propagate it into the publish options.
  • Define a new LiveFlag/Parameter for the publish subcommand
  • Extend the publish() CLI entrypoint to accept --live and document behaviour in the docstring
  • Pass the live flag through to commands.publish.PublishOptions so it controls _publish_crates live mode
lading/cli.py
Introduce reusable BDD infrastructure for publish preflight and publish command stubbing/recording.
  • Add _CommandResponse, _PreflightInvocationRecorder, and PreflightTestContext to capture and inspect stubbed command invocations
  • Provide machinery to normalise cargo commands for cmd-mox expectations, register default and override stubs, and enable CMD_MOX_STUB_ENV_VAR for tests
  • Add tests ensuring _resolve_preflight_expectation normalises cargo subcommands correctly
tests/bdd/steps/test_publish_infrastructure.py
Split publish BDD steps into dedicated given/when/then modules with helpers for asserting publish behaviour, including dry-run/live publish and already-published handling.
  • Move shared logic into test_publish_helpers (plan parsing, staging manifest loading, patch entry extraction, crate order and flag assertions)
  • Create test_publish_given_steps for configuring preflight overrides such as failing cargo check/test and already-uploaded publish responses
  • Create test_publish_when_steps for invoking publish and publish preflight with and without --live/--forbid-dirty using the new infrastructure
  • Create test_publish_then_steps for asserting plan output, staging content, preflight behaviour, cargo package ordering, and cargo publish dry-run/live behaviour
tests/bdd/steps/test_publish_helpers.py
tests/bdd/steps/test_publish_infrastructure.py
tests/bdd/steps/test_publish_given_steps.py
tests/bdd/steps/test_publish_when_steps.py
tests/bdd/steps/test_publish_then_steps.py
tests/bdd/conftest.py
tests/bdd/features/cli.feature
Update documentation and roadmap to reflect the new publish flow and completion of cargo publish execution.
  • Describe the publish step as running cargo publish with dry-run by default and --live for real uploads, and document tolerant handling of already-existing versions
  • Update the usage guide to explain the end-to-end publish workflow, including dry-run default, --live, and behaviour when crate versions already exist
  • Mark the roadmap item for implementing cargo publish execution as completed
docs/lading-design.md
docs/usage-guide.md
docs/roadmap.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Dec 2, 2025

Copy link
Copy Markdown

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 7 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b359bc and 292e17f.

📒 Files selected for processing (2)
  • lading/commands/publish.py (9 hunks)
  • tests/unit/publish/test_packaging.py (3 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • Added --live flag to switch from dry-run to actual package publishing via cargo publish
    • Publishing now continues past already-published crate versions instead of aborting
  • Documentation

    • Updated design documentation to clarify publish behaviour with dry-run defaults and live mode
    • Updated roadmap to mark publish implementation as complete
    • Enhanced usage guide with staging workflow and error-tolerant publishing details

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Add a --live option to the publish command (defaults to dry-run). Change publish flow to treat already-published crate versions as warnings and continue to remaining crates. Split and expand BDD publish tests into modular fixtures, helpers, and given/when/then step modules; add unit tests for packaging and publish behaviours.

Changes

Cohort / File(s) Summary
Documentation Updates
docs/lading-design.md, docs/roadmap.md, docs/usage-guide.md
Clarify that cargo publish defaults to dry-run and that --live triggers real uploads; document tolerant handling of already-published crate versions and update publish workflow narrative and roadmap status.
CLI Enhancement
lading/cli.py
Add LiveFlag annotation and new --live parameter on the publish command; forward live into PublishOptions.
Publishing Logic
lading/commands/publish.py
Add module logger, PublishOptions.live, constants and helpers for detecting already-published registry errors, _format_cargo_failure_message(), and _publish_crates() to run cargo publish in dry-run or live mode while skipping already-published crates and raising on other failures. Refactor packaging failure formatting to use the new helper.
BDD Test Plugin Registration
tests/conftest.py, tests/bdd/conftest.py, tests/bdd/steps/test_common_steps.py, tests/bdd/features/cli.feature
Replace monolithic test_publish_steps plugin with four new modules; register new BDD scenarios for dry-run publish, live publish and skipping already-published crates.
BDD Test Infrastructure & Fixtures
tests/bdd/steps/test_publish_infrastructure.py, tests/bdd/steps/test_publish_fixtures.py, tests/bdd/steps/test_publish_helpers.py
Introduce command-response modelling, invocation recorder, preflight stub config and test context, fixtures for preflight overrides and recorder, and helpers to parse publish plans and assert invocation sequences.
BDD Step Definitions (Given/When/Then)
tests/bdd/steps/test_publish_given_steps.py, tests/bdd/steps/test_publish_when_steps.py, tests/bdd/steps/test_publish_then_steps.py
Split publish step definitions into given/when/then modules; add environment setup, preflight override steps, publish invocation steps (including live variant) and detailed then-step assertions for plan output, staging and invocation ordering.
Removed Monolithic Steps
tests/bdd/steps/test_publish_steps.py
Remove previous monolithic publish-step module; redistribute its functionality into the new modular test files.
Unit Tests for Packaging & Publishing
tests/unit/publish/test_packaging.py
Add publish_plan_and_prep fixture, runner utilities (CallTrackingRunner, make_failing_runner()), and tests covering packaging order, failure reporting, dry-run and live publishing, handling of already-published versions, and failure propagation.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "lading publish (CLI)"
    participant Publish as "publish module"
    participant Preflight as "preflight checks"
    participant Cargo as "cargo"
    participant Registry as "registry"

    CLI->>Publish: run(options={live:false|true})
    activate Publish
    Publish->>Preflight: perform preflight (check/test/package)
    activate Preflight
    Preflight->>Cargo: cargo check/test/package
    Preflight-->>Publish: preflight results
    deactivate Preflight

    loop per crate
        Publish->>Cargo: cargo publish --dry-run (default)
        activate Cargo
        Cargo->>Registry: validate publish
        Registry-->>Cargo: dry-run ok / errors
        Cargo-->>Publish: result
        deactivate Cargo

        alt live=true
            Publish->>Cargo: cargo publish (live)
            activate Cargo
            Cargo->>Registry: upload crate
            alt crate already exists
                Registry-->>Cargo: already-published error
                Cargo-->>Publish: error (detectable via code/markers)
                Note over Publish: log warning and continue next crate
            else success
                Registry-->>Cargo: success
                Cargo-->>Publish: success
            end
            deactivate Cargo
        end
    end

    Publish-->>CLI: finish (success/failure)
    deactivate Publish
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Inspect lading/commands/publish.py for correctness of registry error detection, marker matching and exit-code handling.
  • Verify --live flag propagation from lading/cli.py into PublishOptions and into _publish_crates().
  • Review BDD test infrastructure (tests/bdd/steps/test_publish_infrastructure.py, test_publish_helpers.py) for accurate command normalisation, handler behaviour and recorder semantics.
  • Confirm given/when/then step modules use fixtures consistently and tests/conftest.py plugin list matches.
  • Validate unit tests in tests/unit/publish/test_packaging.py exercise both dry-run and live flows and failure paths.

Poem

🚀 Ship the crates with careful art,
Dry-runs first, then play your part,
If versions live, just note and smile,
Keep sailing on, crate after crate, mile after mile. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarises the main change: adding cargo publish with dry-run as the default and introducing a --live option for real publishing.
Docstring Coverage ✅ Passed Docstring coverage is 92.55% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description comprehensively relates to the changeset, detailing dry-run defaults, live publishing, already-published tolerance, and integration with the publish workflow across docs, code, and tests.

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 2, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/bdd/steps/test_publish_steps.py

Comment on file

    cmd_mox: CmdMox
    overrides: dict[tuple[str, ...], _CommandResponse] = dc.field(default_factory=dict)
    overrides: dict[tuple[str, ...], ResponseProvider] = dc.field(default_factory=dict)

❌ New issue: Low Cohesion
This module has at least 8 different responsibilities amongst its 57 functions, threshold = 4

@leynos

leynos commented Dec 2, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/bdd/steps/test_publish_steps.py

Comment on file

    repo_root: Path,
    cmd_mox: CmdMox,
    preflight_overrides: dict[tuple[str, ...], _CommandResponse],
    preflight_overrides: dict[tuple[str, ...], ResponseProvider],

❌ New issue: Code Duplication
The module contains 6 functions with similar structure: then_publish_excludes_preflight_crate,then_publish_limits_preflight_targets,then_publish_runs_dry_run,then_publish_runs_live and 2 more functions

@leynos

leynos commented Dec 2, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/unit/publish/test_packaging.py

Comment on lines +291 to +318

def test_publish_crates_raise_on_failure(tmp_path: Path) -> None:
    """Unexpected cargo publish failures abort the workflow."""
    workspace_root = tmp_path / "workspace"
    crates = make_dependency_chain(workspace_root)
    plan = publish.plan_publication(
        make_workspace(workspace_root, *crates), make_config()
    )
    staging_root = _prepare_staging_root(plan, tmp_path)
    preparation = publish.PublishPreparation(
        staging_root=staging_root,
        copied_readmes=(),
    )

    def failing_runner(
        command: cabc.Sequence[str],
        *,
        cwd: Path | None = None,
        env: cabc.Mapping[str, str] | None = None,
    ) -> tuple[int, str, str]:
        del command, cwd, env
        return 1, "network offline", ""

    with pytest.raises(publish.PublishPreflightError) as excinfo:
        publish._publish_crates(plan, preparation, runner=failing_runner, live=False)

    message = str(excinfo.value)
    assert "cargo publish failed for crate" in message
    assert "network offline" in message

❌ New issue: Code Duplication
The module contains 7 functions with similar structure: test_package_publishable_crates_prefers_stderr_over_stdout,test_package_publishable_crates_reports_stdout_on_failure,test_package_publishable_crates_runs_in_plan_order,test_package_publishable_crates_stops_on_failure and 3 more functions

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented Dec 2, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/bdd/steps/test_publish_steps.py

Comment on file

    publish_command: tuple[str, ...]
    publish_response: ResponseProvider
    for command, response in config.overrides.items():
        if len(command) >= 2 and command[0] == "cargo" and command[1] == "publish":

❌ New issue: Complex Conditional
_invoke_publish_with_options has 1 complex conditionals with 2 branches, threshold = 2

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Refactor publish-related tests to use new helper functions and classes for
tracking command invocations and handling failures. Added fixtures to
simplify test setup and reduce code duplication. Replaced manual checks
on command invocations with reusable assertions, improving readability
and maintainability of test code.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai

This comment was marked as resolved.

…dules

Refactor the publish BDD tests by deleting the large monolithic `test_publish_steps.py` and
replacing it with multiple focused modules:
- `test_publish_fixtures.py` for shared pytest fixtures
- `test_publish_given_steps.py` for Given steps
- `test_publish_when_steps.py` for When steps
- `test_publish_then_steps.py` for Then steps
- `test_publish_helpers.py` for shared helper utilities
- `test_publish_infrastructure.py` for infra helpers and cmd-mox stubs

Also updated imports and test conftest registrations to reflect the new modular layout.

This improves test code organization, maintainability, and readability.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 3, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/bdd/steps/test_publish_helpers.py

Comment on lines +134 to +143

def _assert_invocations_have_flag(
    invocations: list[tuple[tuple[str, ...], dict[str, str]]],
    flag: str,
    command_name: str,
) -> None:
    """Assert that every invocation contains ``flag``."""
    for args, _env in invocations:
        if flag not in args:
            message = f"Expected {flag!r} in {command_name} invocation"
            raise AssertionError(message)

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: _assert_invocations_have_flag,_assert_invocations_lack_flag

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented Dec 3, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/bdd/steps/test_publish_when_steps.py

Comment on lines +40 to +49

def when_invoke_lading_publish(
    workspace_directory: Path,
    repo_root: Path,
    cmd_mox: _ImportedCmdMox,
    preflight_overrides: dict[tuple[str, ...], ResponseProvider],
    preflight_recorder: _PreflightInvocationRecorder,
) -> dict[str, typ.Any]:
    """Execute the publish CLI via ``python -m`` and capture the result."""
    stub_config = _create_stub_config(cmd_mox, preflight_overrides, preflight_recorder)
    return _invoke_publish_with_options(repo_root, workspace_directory, stub_config)

❌ New issue: Excess Number of Function Arguments
when_invoke_lading_publish has 5 arguments, max arguments = 4
when_invoke_lading_publish_forbid_dirty has 5 arguments, max arguments = 4
when_invoke_lading_publish_live has 5 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

…single helper function

Refactored _assert_invocations_have_flag and _assert_invocations_lack_flag to utilize a unified helper _assert_invocations_flag_presence that handles both presence and absence checks for flags in invocations. This removes code duplication and clarifies assertion logic in test_publish_helpers.py.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

…t test setup

- Added PreflightTestContext dataclass to encapsulate cmd_mox, overrides, and recorder
- Replaced separate fixtures and parameters with PreflightTestContext in test steps
- Simplified stub config creation via method on PreflightTestContext
- Improved code clarity and reduced duplication in BDD tests for publish preflight checks

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Code Duplication

tests/unit/publish/test_packaging.py:

What lead to degradation?

The module contains 2 functions with similar structure: test_package_publishable_crates_prefers_stderr_over_stdout,test_package_publishable_crates_reports_stdout_on_failure

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

Refactored failure message assertions in packaging tests by introducing a helper function `_assert_packaging_failure_message_contains`. This reduces code duplication and improves clarity in testing error message contents when packaging crates fails.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/unit/publish/test_packaging.py

Comment on lines +91 to +110

def _assert_packaging_failure_message_contains(
    plan: publish.PublishPlan,
    preparation: publish.PublishPreparation,
    runner: cabc.Callable[..., tuple[int, str, str]],
    expected_in_message: str,
    not_expected_in_message: str | None = None,
) -> None:
    """Assert that packaging failure produces expected error message content."""
    with pytest.raises(publish.PublishPreflightError) as excinfo:
        publish._package_publishable_crates(
            plan,
            preparation,
            runner=runner,
        )

    message = str(excinfo.value)
    assert "cargo package failed for crate alpha" in message
    assert expected_in_message in message
    if not_expected_in_message is not None:
        assert not_expected_in_message not in message

❌ New issue: Excess Number of Function Arguments
_assert_packaging_failure_message_contains has 5 arguments, max arguments = 4

@sourcery-ai sourcery-ai 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.

Hey there - I've reviewed your changes - here's some feedback:

  • The _is_already_published_error helper treats any non-zero exit code whose output contains broad markers like "already exists" as non-fatal, which risks masking unrelated failures; consider tightening the condition (e.g. checking for known cargo exit codes like 101 and/or more specific message patterns) to reduce false positives.
  • The error handling and message construction in _publish_crates closely mirrors _package_publishable_crates; factoring the common logic into a shared helper (e.g. for picking stderr/stdout and formatting PublishPreflightError messages) would reduce duplication and keep behavior consistent between packaging and publishing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_is_already_published_error` helper treats any non-zero exit code whose output contains broad markers like `"already exists"` as non-fatal, which risks masking unrelated failures; consider tightening the condition (e.g. checking for known cargo exit codes like 101 and/or more specific message patterns) to reduce false positives.
- The error handling and message construction in `_publish_crates` closely mirrors `_package_publishable_crates`; factoring the common logic into a shared helper (e.g. for picking stderr/stdout and formatting `PublishPreflightError` messages) would reduce duplication and keep behavior consistent between packaging and publishing.

## Individual Comments

### Comment 1
<location> `lading/commands/publish.py:364-367` </location>
<code_context>
             raise PublishPreflightError(message)


+_ALREADY_PUBLISHED_MARKERS: tuple[str, ...] = (
+    "already uploaded",
+    "already published",
+    "already exists",
+)
+
</code_context>

<issue_to_address>
**issue (bug_risk):** The "already exists" marker looks broad and might classify unrelated failures as safe-to-ignore.

Matching the generic substring "already exists" on combined stdout/stderr risks treating unrelated errors as harmless "already published" cases (e.g., registry or filesystem failures that include that phrase). Consider tightening this to cargo’s specific publish error wording or including more surrounding context so only true "version already on the registry" errors are downgraded.
</issue_to_address>

### Comment 2
<location> `docs/usage-guide.md:206` </location>
<code_context>
 ```

+Append `--live` to replace the default dry-run with real `cargo publish`
+invocations when you are ready to ship.
+
 Example output:
</code_context>

<issue_to_address>
**issue (review_instructions):** The phrase "when you are ready" uses a second-person pronoun and should be rewritten in neutral form.

To comply with the style guidance, please avoid "you" here. For example, this could be rephrased as "when the release is ready to ship" or similar neutral wording.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Avoid 2nd person or 1st person pronouns ("I", "you", "we").

</details>
</issue_to_address>

### Comment 3
<location> `docs/usage-guide.md:234` </location>
<code_context>
-behaviour is a dry-run that validates packaging only.
+workspace, the command runs `cargo package` for every publishable crate in
+plan order inside the staged copy. After packaging, Lading invokes
+`cargo publish --dry-run` for each crate so you can validate the full pipeline
+without uploading. Use `--live` to omit the `--dry-run` flag and perform the
+actual publication. If the registry reports that a crate version already
</code_context>

<issue_to_address>
**issue (review_instructions):** The clause "so you can validate" uses second-person voice and should be rephrased.

Consider rewriting this to avoid addressing the reader directly, for example "so the full pipeline can be validated" or similar neutral phrasing.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Avoid 2nd person or 1st person pronouns ("I", "you", "we").

</details>
</issue_to_address>

### Comment 4
<location> `tests/bdd/conftest.py:9` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 5
<location> `tests/bdd/conftest.py:10` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 6
<location> `tests/bdd/conftest.py:11` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 7
<location> `tests/bdd/conftest.py:12` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 8
<location> `tests/bdd/conftest.py:13` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 9
<location> `tests/bdd/conftest.py:14` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 10
<location> `tests/bdd/steps/test_publish_helpers.py:44` </location>
<code_context>
    return {} if not isinstance(crates_io, typ.Mapping) else dict(crates_io)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Swap if/else branches of if expression to remove negation ([`swap-if-expression`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/swap-if-expression))

```suggestion
    return dict(crates_io) if isinstance(crates_io, typ.Mapping) else {}
```

<br/><details><summary>Explanation</summary>Negated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By swapping the `if` and `else` conditions around we
can invert the condition and make it positive.
</details>
</issue_to_address>

### Comment 11
<location> `tests/bdd/steps/test_publish_helpers.py:105-108` </location>
<code_context>
def _has_contiguous_args(args: tuple[str, ...], first: str, second: str) -> bool:
    """Return True when ``first`` is immediately followed by ``second`` in ``args``."""
    for index in range(len(args) - 1):
        if args[index] == first and args[index + 1] == second:
            return True
    return False

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use any() instead of for loop ([`use-any`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-any/))

```suggestion
    return any(
        args[index] == first and args[index + 1] == second
        for index in range(len(args) - 1)
    )
```
</issue_to_address>

### Comment 12
<location> `tests/bdd/steps/test_publish_infrastructure.py:202` </location>
<code_context>
def _register_preflight_commands(config: _PreflightStubConfig) -> None:
    """Install cmd-mox doubles for publish pre-flight commands."""
    defaults: dict[tuple[str, ...], ResponseProvider] = {
        ("git", "status", "--porcelain"): _CommandResponse(exit_code=0),
        (
            "cargo",
            "check",
            "--workspace",
            "--all-targets",
        ): _CommandResponse(exit_code=0),
        (
            "cargo",
            "test",
            "--workspace",
        ): _CommandResponse(exit_code=0),
        ("cargo", "package"): _CommandResponse(exit_code=0),
    }

    publish_command: tuple[str, ...]
    publish_response: ResponseProvider
    for command, response in config.overrides.items():
        if _is_cargo_publish_command(command):
            publish_command = command
            publish_response = response
            break
    else:
        publish_command = ("cargo", "publish", "--dry-run")
        publish_response = _CommandResponse(exit_code=0)

    filtered_overrides = {
        command: response
        for command, response in config.overrides.items()
        if not _is_cargo_publish_command(command)
    }

    defaults.update(filtered_overrides)
    defaults[publish_command] = publish_response
    for command, response in defaults.items():
        expectation_program, expectation_args = _resolve_preflight_expectation(command)
        config.cmd_mox.stub(expectation_program).runs(
            _make_preflight_handler(
                response, expectation_args, config.recorder, expectation_program
            )
        )

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge dictionary updates via the union operator ([`dict-assign-update-to-union`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/dict-assign-update-to-union/))

```suggestion
    defaults |= filtered_overrides
```
</issue_to_address>

### Comment 13
<location> `tests/bdd/steps/test_publish_then_steps.py:147` </location>
<code_context>
@then(parsers.parse('the cargo test pre-flight env contains "{name}"="{value}"'))
def then_cargo_test_env_contains(
    preflight_recorder: _PreflightInvocationRecorder,
    name: str,
    value: str,
) -> None:
    """Assert that cargo test env propagates ``name`` with ``value``."""
    envs = _get_test_invocation_envs(preflight_recorder)
    if not any(environment.get(name) == value for environment in envs):
        message = f"Expected cargo test env {name}={value!r}"
        raise AssertionError(message)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Invert any/all to simplify comparisons ([`invert-any-all`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/invert-any-all/))

```suggestion
    if all(environment.get(name) != value for environment in envs):
```
</issue_to_address>

### Comment 14
<location> `tests/bdd/steps/test_publish_then_steps.py:159` </location>
<code_context>
@then(parsers.parse('the cargo test pre-flight env includes "{snippet}" in RUSTFLAGS'))
def then_cargo_test_env_rustflags_contains(
    preflight_recorder: _PreflightInvocationRecorder,
    snippet: str,
) -> None:
    """Assert that cargo test RUSTFLAGS contains ``snippet``."""
    envs = _get_test_invocation_envs(preflight_recorder)
    if not any(snippet in environment.get("RUSTFLAGS", "") for environment in envs):
        message = f"Expected {snippet!r} in cargo test RUSTFLAGS"
        raise AssertionError(message)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Invert any/all to simplify comparisons ([`invert-any-all`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/invert-any-all/))

```suggestion
    if all(
        snippet not in environment.get("RUSTFLAGS", "") for environment in envs
    ):
```
</issue_to_address>

### Comment 15
<location> `tests/bdd/steps/test_publish_when_steps.py:77-78` </location>
<code_context>
@when(
    "I invoke lading publish with that workspace using --live",
    target_fixture="cli_run",
)
def when_invoke_lading_publish_live(
    workspace_directory: Path,
    repo_root: Path,
    preflight_test_context: PreflightTestContext,
) -> dict[str, typ.Any]:
    """Execute the publish CLI with live publishing enabled."""
    if not any(
        command[:2] == ("cargo", "publish")
        for command in preflight_test_context.overrides
    ):
        preflight_test_context.overrides[("cargo", "publish")] = _CommandResponse(
            exit_code=0
        )
    stub_config = preflight_test_context.create_stub_config()
    return _invoke_publish_with_options(
        repo_root,
        workspace_directory,
        stub_config,
        "--live",
    )

</code_context>

<issue_to_address>
**suggestion (code-quality):** Invert any/all to simplify comparisons ([`invert-any-all`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/invert-any-all/))

```suggestion
    if all(
        command[:2] != ("cargo", "publish")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lading/commands/publish.py Outdated
Comment thread docs/usage-guide.md Outdated
Comment thread docs/usage-guide.md Outdated
Comment thread tests/bdd/conftest.py Outdated
Comment thread tests/bdd/conftest.py Outdated
Comment thread tests/bdd/steps/test_publish_helpers.py Outdated
Comment thread tests/bdd/steps/test_publish_infrastructure.py Outdated
Comment thread tests/bdd/steps/test_publish_then_steps.py Outdated
Comment thread tests/bdd/steps/test_publish_then_steps.py Outdated
Comment thread tests/bdd/steps/test_publish_when_steps.py Outdated

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 752c9ad and ee9c891.

📒 Files selected for processing (17)
  • docs/lading-design.md (1 hunks)
  • docs/roadmap.md (1 hunks)
  • docs/usage-guide.md (2 hunks)
  • lading/cli.py (3 hunks)
  • lading/commands/publish.py (6 hunks)
  • tests/bdd/conftest.py (1 hunks)
  • tests/bdd/features/cli.feature (1 hunks)
  • tests/bdd/steps/test_common_steps.py (1 hunks)
  • tests/bdd/steps/test_publish_fixtures.py (1 hunks)
  • tests/bdd/steps/test_publish_given_steps.py (1 hunks)
  • tests/bdd/steps/test_publish_helpers.py (1 hunks)
  • tests/bdd/steps/test_publish_infrastructure.py (1 hunks)
  • tests/bdd/steps/test_publish_steps.py (0 hunks)
  • tests/bdd/steps/test_publish_then_steps.py (1 hunks)
  • tests/bdd/steps/test_publish_when_steps.py (1 hunks)
  • tests/conftest.py (1 hunks)
  • tests/unit/publish/test_packaging.py (3 hunks)
💤 Files with no reviewable changes (1)
  • tests/bdd/steps/test_publish_steps.py
🧰 Additional context used
📓 Path-based instructions (5)
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Use the markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
Proactively update relevant files in the docs/ directory when new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve to keep documentation accurate and current.

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict) with suffixes: -ize in words like 'realize' and 'organization', -lyse in words like 'analyse' and 'paralyse', -our in words like 'colour' and 'behaviour', -re in words like 'centre' and 'calibre', double 'l' in words like 'cancelled' and 'counsellor', maintain 'e' in words like 'likeable', -ogue in words like 'catalogue'
Use the Oxford comma in documentation: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns in documentation, for example 'Lille Industries are expanding'
Write headings in sentence case
Use Markdown headings (#, ##, ###, and so on) in order without skipping levels
Follow markdownlint recommendations for Markdown formatting
Always provide a language identifier for fenced code blocks; use 'plaintext' for non-code text
Use '-' as the first level bullet and renumber lists when items change in Markdown
Prefer inline links using text or angle brackets around the URL in Markdown
Ensure blank lines before and after bulleted lists and fenced blocks in Markdown
Ensure tables have a delimiter line below the header row in Markdown
Expand any uncommon acronym on first use, for example, Continuous Integration (CI)
Wrap paragraphs at 80 columns in documentation
Wrap code at 120 columns in documentation
Do not wrap tables in documentation
Use footnotes referenced with [^label] in Markdown documentation
Include Mermaid diagrams in documentation where they add clarity
When embedding figures in documentation, use ![alt text](path/to...

Files:

  • docs/lading-design.md
  • docs/usage-guide.md
  • docs/roadmap.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: For Markdown files (.md only), ensure changes pass lint checks via make markdownlint.
For Markdown files (.md only), ensure Mermaid diagrams pass validation using nixie via make nixie.

Files:

  • docs/lading-design.md
  • docs/usage-guide.md
  • docs/roadmap.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/lading-design.md
  • docs/usage-guide.md
  • docs/roadmap.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: For Python files, ensure changes pass all relevant unit and behavioral tests according to testing guidelines.
For Python files, ensure changes pass lint checks via make lint.
For Python files, ensure changes adhere to formatting standards via make check-fmt.
For Python files, ensure changes pass type checking via make typecheck.
For Python development, refer to detailed guidelines in the .rules/ directory including Python Code Style Guidelines, Context Managers, Exception Design/Raising/Handling/Logging, Generators, Project Configuration, Return Patterns, and Typing best practices.

**/*.py: Use snake_case.py for Python file names, naming them for their contents
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix private or internal helpers with a single underscore (_)
Enable and maintain full static type coverage using Pyright; use typing everywhere
Use TypedDict or @DataClass(slots=True) for structured data
Avoid Any; use precise types (TypeVar, Protocol, Literal, Union) and typing.cast only when necessary with justification
Be explicit with return type annotations using -> None, -> str, etc., for all public functions and class methods
Prefer immutability: use tuples over lists and types.MappingProxyType for read-only mappings
Enforce strict mode in Pyright; treat all Pyright warnings as CI errors and use # pyright: ignore sparingly with explanation
Avoid side effects at import time; modules should not modify global state or perform actions on import
Document public functions, classes, and modules using NumPy-format docstrings
Use inline comments to explain tricky code and non-obvious logic

**/*.py: Use context managers (via contextlib.contextmanager or __enter__/__exit__ methods) to encapsulate setup and teardown logic for resource management (files, locks, connections, etc.)
Use @contextlib.contextmanager decorator for straightforward procedural setup...

Files:

  • tests/bdd/steps/test_publish_fixtures.py
  • lading/cli.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/conftest.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/steps/test_publish_when_steps.py
  • tests/bdd/steps/test_publish_then_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_common_steps.py
  • lading/commands/publish.py
  • tests/bdd/steps/test_publish_helpers.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep C90 / mccabe complexity ≤ 9

  • Follow single responsibility and CQRS (command/query segregation)
  • Prefer structural pattern matching to
  • Prefer structural pattern matching over isinstance() or imperative decomposition.
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.
  • All path manipulation must be performed using pathlib for cross platform safety. Do not use string manipulation for extraction of path components or filename ele...

Files:

  • tests/bdd/steps/test_publish_fixtures.py
  • lading/cli.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/conftest.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/steps/test_publish_when_steps.py
  • tests/bdd/steps/test_publish_then_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_common_steps.py
  • lading/commands/publish.py
  • tests/bdd/steps/test_publish_helpers.py
**/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/test_*.py: Use pytest idioms: prefer fixtures over setup/teardown, parametrize broadly, avoid unnecessary mocks
Group related tests using class with method names prefixed by test_
Write tests from a user's perspective; test public behavior, not internals
Avoid mocking too much; prefer test doubles only for external services or non-deterministic behaviors

Files:

  • tests/bdd/steps/test_publish_fixtures.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/steps/test_publish_when_steps.py
  • tests/bdd/steps/test_publish_then_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/steps/test_common_steps.py
  • tests/bdd/steps/test_publish_helpers.py
**/*test*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

In tests, use narrow exception assertions with pytest.raises() specifying the expected type and optionally constraining the message via regex (B017)

Files:

  • tests/bdd/steps/test_publish_fixtures.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/conftest.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/steps/test_publish_when_steps.py
  • tests/bdd/steps/test_publish_then_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_common_steps.py
  • tests/bdd/steps/test_publish_helpers.py
🧬 Code graph analysis (4)
tests/bdd/steps/test_publish_fixtures.py (1)
tests/bdd/steps/test_publish_infrastructure.py (2)
  • PreflightTestContext (68-77)
  • _PreflightInvocationRecorder (40-55)
lading/cli.py (1)
lading/commands/publish.py (1)
  • PublishOptions (67-105)
tests/bdd/steps/test_publish_infrastructure.py (2)
tests/bdd/steps/test_common_steps.py (1)
  • _run_cli (21-46)
lading/commands/publish_execution.py (1)
  • _normalise_cmd_mox_command (185-195)
tests/bdd/steps/test_publish_helpers.py (4)
tests/bdd/steps/test_publish_infrastructure.py (2)
  • _PreflightInvocationRecorder (40-55)
  • by_label (50-55)
tests/unit/conftest.py (1)
  • staging_root (184-186)
lading/testing/toml_utils.py (1)
  • load_manifest (166-188)
crate_tools/unittests/publish_check/conftest.py (1)
  • env (289-292)
🔍 Remote MCP

The document doesn't mention --dry-run. Let me search for specific cargo publish documentation:Perfect! Now let me search for information about Lading specifically:Let me search for Lading repository directly to understand what it is:Based on the PR context, I can see that Lading is a Python project based on the file structure (.py files). Let me compile my findings into a comprehensive summary.

Summary of Relevant Context for PR Review

Based on my research, here are the key facts relevant to reviewing this PR:

Understanding cargo publish --dry-run Behavior

The --dry-run flag performs all checks without uploading. More specifically, it is recommended that you first run cargo publish --dry-run (or cargo package which is equivalent) to ensure there aren't any warnings or errors before publishing. The flag ensures all steps leading up to uploading are performed, but the actual upload is skipped.

Standard Cargo Publish Workflow

The cargo publish command performs: verification checks on the package, compression of source code into a .crate file, extraction and verification that it compiles, and uploading the .crate file to crates.io.

Workspace Publishing Challenges

Each crate in a workspace needs to be published separately. Multi-package publishing was recently promoted to a stable feature, but is currently only available in nightly builds. If you're using a recent nightly build of Cargo 1.90.0 or later, running cargo publish in a workspace will work.

Design Pattern Alignment

The PR's approach of defaulting to dry-run and requiring an explicit flag for live publishing aligns with industry best practices. Similar tools like cargo-release use --no-verify to speed up dry-run by skipping cargo-publish's verify step, showing that staged validation is a common pattern for workspace publishing tools.

Error Handling Consideration

The PR implements detection of already-published crates and continues processing. This is significant because publishing a crate is permanent and the version can never be overwritten, making this error case a normal operational scenario when publishing a workspace with previously-published crates.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review
🔇 Additional comments (28)
docs/roadmap.md (1)

205-212: Mark cargo publish execution as complete with accurate criteria.

Keep this task entry as written; it correctly describes dry‑run default behaviour, live mode, and continuation on already‑published versions, and matches the implemented workflow and tests.

tests/bdd/conftest.py (1)

9-14: Register publish BDD fixtures and steps within the BDD test package.

Retain these imports; they align the BDD step registration with the new split publish fixtures/helpers/steps and follow the established pattern for config, manifest, and metadata fixtures.

tests/bdd/steps/test_common_steps.py (1)

175-179: Align CLI feature step imports with split publish modules.

Keep these three imports; they correctly replace the monolithic publish steps module with given/when/then modules and preserve step registration for the CLI feature.

tests/bdd/features/cli.feature (1)

121-139: Exercise publish dry‑run, live mode, and already‑published handling via CLI scenarios.

Retain these three scenarios; they give clear, end‑to‑end coverage of the default dry‑run behaviour, the --live flag, and continuation when cargo publish reports an existing version for a crate.

docs/lading-design.md (1)

513-517: Document cargo publish dry‑run default, --live, and already‑published semantics.

Keep this updated description; it accurately states that cargo publish runs with --dry-run by default, switches to a real upload with --live, and logs then skips crates whose versions already exist.

tests/conftest.py (1)

18-21: Update pytest plugins to register new publish BDD modules.

Keep this plugin list; it correctly replaces the monolithic publish steps plugin with dedicated fixtures and given/when/then modules, matching the refactored BDD layout while preserving scenario registration.

tests/bdd/steps/test_publish_fixtures.py (1)

1-37: Provide dedicated fixtures for publish preflight overrides and recording.

Leave these fixtures as implemented; they supply fresh override mappings and recorders per scenario and assemble them into a typed PreflightTestContext, giving isolated, reusable scaffolding for publish BDD steps.

docs/usage-guide.md (2)

205-206: LGTM!

The --live flag documentation is clear and concise, accurately reflecting the implementation behaviour.


232-238: LGTM!

The publish sequence documentation correctly describes the dry-run default, live mode opt-in, and the graceful handling of already-published crate versions. The British English spelling and Oxford comma usage are correct.

tests/bdd/steps/test_publish_when_steps.py (1)

23-45: LGTM!

The preflight check step and basic publish invocation step are well-structured with appropriate error capture and fixture wiring.

tests/bdd/steps/test_publish_then_steps.py (5)

1-31: LGTM!

The import structure and type checking guards are well-organised. The helper imports from test_publish_helpers provide good separation of concerns.


33-42: LGTM!

The plan verification step correctly validates the expected output structure.


76-98: LGTM!

The preflight exclusion check and helper function correctly verify --exclude flag presence. The message construction follows the coding guidelines by assigning to a variable before raising.


195-224: LGTM!

The dry-run and live publish verification steps correctly distinguish between the two modes by checking for presence or absence of the --dry-run flag. The crate order assertion provides good coverage.


302-322: LGTM!

The README staging verification correctly compares file contents between the staged and source locations using explicit UTF-8 encoding.

lading/commands/publish.py (4)

48-48: LGTM!

The module-level logger follows the coding guidelines using logging.getLogger(__name__).


364-376: LGTM!

The already-published detection is robust. The markers cover common cargo registry error messages, and the case-insensitive search combining stdout and stderr ensures reliable detection across cargo versions.


391-408: LGTM!

The logging follows coding guidelines with parameterised lazy interpolation. The warning for already-published crates includes both crate name and version, providing sufficient context for operators.


485-490: LGTM!

The integration correctly passes the live flag from effective_options and maintains the expected execution order: preflight → plan → prepare → strip patches → package → publish.

tests/unit/publish/test_packaging.py (6)

34-49: LGTM!

The fixture provides a clean 3-tuple pattern allowing tests to unpack only what they need. The staging root preparation correctly mirrors the plan structure.


52-69: LGTM!

CallTrackingRunner provides a clean test double for verifying command invocations. The call recording captures both the command tuple and working directory, enabling precise assertions.


91-112: LGTM with minor observation.

The helper correctly validates failure messages. The 2-tuple parameter requires callers to slice the 3-tuple fixture, which is slightly awkward but acceptable. The PR comments mention consolidating this parameter - that refactor would improve ergonomics.


190-209: LGTM!

The dry-run test correctly verifies that --dry-run is passed to each cargo publish invocation and that logging occurs. The caplog fixture usage follows pytest idioms.


228-266: LGTM!

Excellent coverage for the already-published continuation behaviour. The test correctly simulates the registry error for alpha and verifies that beta still publishes. The warning log assertion confirms the user-facing feedback.


269-281: LGTM!

The failure test verifies that unexpected cargo publish errors abort the workflow with appropriate error details.

tests/bdd/steps/test_publish_given_steps.py (1)

1-130: Keep the decomposed given-step module structure

Keep this file as the focused “given” step module: the separation of cmd-mox socket setup, preflight overrides, and failure simulations is clear, type-safe, and lines up cleanly with the shared infrastructure and fixtures.

tests/bdd/steps/test_publish_helpers.py (1)

134-169: Reuse _assert_invocations_flag_presence to centralise flag checks

Keep _assert_invocations_have_flag and _assert_invocations_lack_flag delegating to _assert_invocations_flag_presence; this de-duplicates the previous flag-checking logic and keeps dry-run vs live assertions simple and uniform across BDD steps.

tests/bdd/steps/test_publish_infrastructure.py (1)

234-259: Retain parametrised coverage of _resolve_preflight_expectation

Keep this parametrised test: it tightly couples _resolve_preflight_expectation to publish._normalise_cmd_mox_command semantics and guards a wide range of cargo subcommands with and without extra arguments, which is valuable protection for the preflight stub wiring.

Comment thread lading/cli.py
Comment thread lading/commands/publish.py Outdated
Comment thread tests/bdd/steps/test_publish_given_steps.py
Comment thread tests/bdd/steps/test_publish_helpers.py Outdated
Comment thread tests/bdd/steps/test_publish_infrastructure.py Outdated
Comment thread tests/bdd/steps/test_publish_infrastructure.py
Comment thread tests/bdd/steps/test_publish_infrastructure.py
Comment thread tests/bdd/steps/test_publish_when_steps.py Outdated
Comment thread tests/bdd/steps/test_publish_when_steps.py
Comment thread tests/unit/publish/test_packaging.py
@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following now been resolved?

  • The _is_already_published_error helper treats any non-zero exit code whose output contains broad markers like "already exists" as non-fatal, which risks masking unrelated failures; consider tightening the condition (e.g. checking for known cargo exit codes like 101 and/or more specific message patterns) to reduce false positives.
  • The error handling and message construction in _publish_crates closely mirrors _package_publishable_crates; factoring the common logic into a shared helper (e.g. for picking stderr/stdout and formatting PublishPreflightError messages) would reduce duplication and keep behavior consistent between packaging and publishing.

@coderabbitai

This comment was marked as resolved.

- Add precise markers for already published crates in publish command
- Replace tuple addition with set union for defaults dictionary update
- Improve test helpers with clearer logic and better type usage
- Amend usage guide wording for clarity on publish command
- Remove unnecessary imports and clean BDD conftest

These changes improve robustness of publishing checks and maintain code clarity.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Introduced a helper function `_format_cargo_failure_message` to unify and simplify
error messages from cargo command failures during package and publish steps.
Refactored existing code to use this helper, reducing duplication and improving
consistency of messages. Additionally, added clarifying comments around the
already-published error detection logic for cargo exit code 101.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@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: 6

♻️ Duplicate comments (4)
tests/bdd/steps/test_publish_helpers.py (1)

16-27: Handle malformed staging lines defensively.

The split(": ", 1) call assumes the separator exists after the assertion. If the staging line is malformed (e.g., "Staged workspace at" without the colon), split returns a single-element list and [1] raises IndexError. Add a guard for clearer failure diagnostics.

 def _extract_staging_root_from_plan(lines: list[str]) -> Path:
     """Return the staging root path parsed from publish plan ``lines``."""
     staging_line = next(
         (line for line in lines if line.startswith("Staged workspace at:")), None
     )
     assert staging_line is not None, "Staging location not found in publish plan output"
-    return Path(staging_line.split(": ", 1)[1])
+    parts = staging_line.split(": ", 1)
+    assert len(parts) == 2, f"Malformed staging line: {staging_line!r}"
+    return Path(parts[1])
tests/bdd/steps/test_publish_infrastructure.py (3)

67-77: Add slots=True to PreflightTestContext.

Per prior feedback, declare with slots=True to match other dataclasses in this module and reduce per-instance overhead.

-@dc.dataclass
+@dc.dataclass(slots=True)
 class PreflightTestContext:

167-210: Document the single cargo publish override behaviour.

Per prior feedback, clarify in the docstring that only the first cargo publish override is honoured.

 def _register_preflight_commands(config: _PreflightStubConfig) -> None:
-    """Install cmd-mox doubles for publish pre-flight commands."""
+    """Install cmd-mox doubles for publish pre-flight commands.
+
+    Notes
+    -----
+    Only a single cargo publish override is honoured. If multiple cargo
+    publish entries appear in ``config.overrides``, the first wins.
+    """

213-231: Use a context manager for environment variable toggling.

Per prior feedback, replace the manual try/finally with a context manager for cleaner resource management. The current implementation is functionally correct but could be cleaner.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee9c891 and 725405b.

📒 Files selected for processing (8)
  • docs/usage-guide.md (2 hunks)
  • lading/commands/publish.py (7 hunks)
  • tests/bdd/conftest.py (1 hunks)
  • tests/bdd/steps/test_publish_helpers.py (1 hunks)
  • tests/bdd/steps/test_publish_infrastructure.py (1 hunks)
  • tests/bdd/steps/test_publish_then_steps.py (1 hunks)
  • tests/bdd/steps/test_publish_when_steps.py (1 hunks)
  • tests/unit/publish/test_packaging.py (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Use the markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
Proactively update relevant files in the docs/ directory when new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve to keep documentation accurate and current.

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict) with suffixes: -ize in words like 'realize' and 'organization', -lyse in words like 'analyse' and 'paralyse', -our in words like 'colour' and 'behaviour', -re in words like 'centre' and 'calibre', double 'l' in words like 'cancelled' and 'counsellor', maintain 'e' in words like 'likeable', -ogue in words like 'catalogue'
Use the Oxford comma in documentation: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns in documentation, for example 'Lille Industries are expanding'
Write headings in sentence case
Use Markdown headings (#, ##, ###, and so on) in order without skipping levels
Follow markdownlint recommendations for Markdown formatting
Always provide a language identifier for fenced code blocks; use 'plaintext' for non-code text
Use '-' as the first level bullet and renumber lists when items change in Markdown
Prefer inline links using text or angle brackets around the URL in Markdown
Ensure blank lines before and after bulleted lists and fenced blocks in Markdown
Ensure tables have a delimiter line below the header row in Markdown
Expand any uncommon acronym on first use, for example, Continuous Integration (CI)
Wrap paragraphs at 80 columns in documentation
Wrap code at 120 columns in documentation
Do not wrap tables in documentation
Use footnotes referenced with [^label] in Markdown documentation
Include Mermaid diagrams in documentation where they add clarity
When embedding figures in documentation, use ![alt text](path/to...

Files:

  • docs/usage-guide.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: For Markdown files (.md only), ensure changes pass lint checks via make markdownlint.
For Markdown files (.md only), ensure Mermaid diagrams pass validation using nixie via make nixie.

Files:

  • docs/usage-guide.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/usage-guide.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: For Python files, ensure changes pass all relevant unit and behavioral tests according to testing guidelines.
For Python files, ensure changes pass lint checks via make lint.
For Python files, ensure changes adhere to formatting standards via make check-fmt.
For Python files, ensure changes pass type checking via make typecheck.
For Python development, refer to detailed guidelines in the .rules/ directory including Python Code Style Guidelines, Context Managers, Exception Design/Raising/Handling/Logging, Generators, Project Configuration, Return Patterns, and Typing best practices.

**/*.py: Use snake_case.py for Python file names, naming them for their contents
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix private or internal helpers with a single underscore (_)
Enable and maintain full static type coverage using Pyright; use typing everywhere
Use TypedDict or @DataClass(slots=True) for structured data
Avoid Any; use precise types (TypeVar, Protocol, Literal, Union) and typing.cast only when necessary with justification
Be explicit with return type annotations using -> None, -> str, etc., for all public functions and class methods
Prefer immutability: use tuples over lists and types.MappingProxyType for read-only mappings
Enforce strict mode in Pyright; treat all Pyright warnings as CI errors and use # pyright: ignore sparingly with explanation
Avoid side effects at import time; modules should not modify global state or perform actions on import
Document public functions, classes, and modules using NumPy-format docstrings
Use inline comments to explain tricky code and non-obvious logic

**/*.py: Use context managers (via contextlib.contextmanager or __enter__/__exit__ methods) to encapsulate setup and teardown logic for resource management (files, locks, connections, etc.)
Use @contextlib.contextmanager decorator for straightforward procedural setup...

Files:

  • tests/bdd/steps/test_publish_when_steps.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_publish_helpers.py
  • lading/commands/publish.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/steps/test_publish_then_steps.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep C90 / mccabe complexity ≤ 9

  • Follow single responsibility and CQRS (command/query segregation)
  • Prefer structural pattern matching to
  • Prefer structural pattern matching over isinstance() or imperative decomposition.
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.
  • All path manipulation must be performed using pathlib for cross platform safety. Do not use string manipulation for extraction of path components or filename ele...

Files:

  • tests/bdd/steps/test_publish_when_steps.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_publish_helpers.py
  • lading/commands/publish.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/steps/test_publish_then_steps.py
**/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/test_*.py: Use pytest idioms: prefer fixtures over setup/teardown, parametrize broadly, avoid unnecessary mocks
Group related tests using class with method names prefixed by test_
Write tests from a user's perspective; test public behavior, not internals
Avoid mocking too much; prefer test doubles only for external services or non-deterministic behaviors

Files:

  • tests/bdd/steps/test_publish_when_steps.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/steps/test_publish_helpers.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/steps/test_publish_then_steps.py
**/*test*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

In tests, use narrow exception assertions with pytest.raises() specifying the expected type and optionally constraining the message via regex (B017)

Files:

  • tests/bdd/steps/test_publish_when_steps.py
  • tests/unit/publish/test_packaging.py
  • tests/bdd/conftest.py
  • tests/bdd/steps/test_publish_helpers.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/bdd/steps/test_publish_then_steps.py
🧬 Code graph analysis (2)
lading/commands/publish.py (4)
lading/commands/publish_plan.py (1)
  • PublishPlan (22-34)
tests/unit/publish/test_packaging.py (1)
  • runner (244-259)
lading/commands/publish_execution.py (1)
  • _CommandRunner (52-62)
tests/unit/conftest.py (1)
  • staging_root (184-186)
tests/bdd/steps/test_publish_then_steps.py (3)
tests/bdd/steps/test_publish_helpers.py (14)
  • _assert_crate_order_matches (172-183)
  • _assert_invocations_have_flag (150-158)
  • _assert_invocations_lack_flag (161-169)
  • _extract_crate_names_from_invocations (82-90)
  • _extract_staging_root_from_plan (21-27)
  • _get_package_invocations (62-69)
  • _get_patch_entries (38-44)
  • _get_publish_invocations (72-79)
  • _get_test_invocation_envs (93-100)
  • _get_test_invocations (52-59)
  • _has_contiguous_args (103-108)
  • _load_staged_manifest (30-35)
  • _publish_plan_lines (16-18)
  • _split_names (47-49)
tests/bdd/steps/test_publish_infrastructure.py (2)
  • _PreflightInvocationRecorder (40-55)
  • by_label (50-55)
lading/commands/publish.py (1)
  • PublishPreflightError (116-117)
🪛 LanguageTool
docs/usage-guide.md

[uncategorized] ~234-~234: Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...cargo publish --dry-run for each crate so the full pipeline can be validated with...

(COMMA_COMPOUND_SENTENCE_2)

🔍 Remote MCP Ref

Summary of additional facts relevant to this PR

  • cargo publish --dry-run performs full verification steps (packaging, verify/compile) but does not upload; using dry-run before a real publish is recommended because publish is permanent and versions cannot be overwritten.

  • A real cargo publish uploads the .crate and is permanent; therefore treating "already published" as a non-fatal, expected condition (log-and-continue) is a reasonable workflow choice for workspace-wide publication tools that iterate crates.

Tools used: Ref (Ref_ref_search_documentation, Ref_ref_read_url)

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (44)
tests/bdd/conftest.py (1)

1-6: Keep this conftest as a documented, minimal placeholder.

The docstring clearly explains that BDD step and fixture registration happens via tests/conftest.py, and the file has no side-effect imports, which resolves earlier static-analysis noise without affecting behaviour. Leave this structure as-is.

docs/usage-guide.md (1)

205-206: LGTM!

The documentation correctly describes the --live flag behaviour and uses neutral phrasing ("once a release is ready to ship") consistent with the style guidelines.

tests/unit/publish/test_packaging.py (8)

34-49: LGTM!

The publish_plan_and_prep fixture cleanly encapsulates the common setup pattern, reducing duplication across tests. The return type annotation is explicit and matches the fixture's actual return value.


52-69: LGTM!

CallTrackingRunner correctly implements the _CommandRunner protocol signature, including the keyword-only cwd and env parameters. Recording (tuple(command), cwd) provides sufficient traceability for assertions.


72-86: LGTM!

The return type Callable[..., tuple[int, str, str]] addresses the previous review comment about the annotation mismatch. The ellipsis correctly indicates the inner function accepts additional keyword arguments.


89-109: LGTM!

The helper consolidates repeated assertion logic effectively. The optional not_expected_in_message parameter handles both positive and negative content checks in a single function.


188-208: LGTM!

The test verifies dry-run behaviour including the --dry-run flag presence and info-level logging. The log capture setup with caplog.set_level is appropriate for asserting operational output.


210-223: LGTM!

The live-mode test correctly asserts that --dry-run is absent from the command tuple when live=True.


226-264: LGTM!

The already-published continuation test correctly simulates exit code 101 with the "already uploaded" message, verifying that:

  1. Processing continues to the next crate
  2. A warning is logged
  3. No exception is raised

This aligns with the documented behaviour for handling existing crate versions.


267-279: LGTM!

The failure test validates that unexpected errors (without already-published markers) raise PublishPreflightError with appropriate message content.

lading/commands/publish.py (6)

48-48: LGTM!

Module-level logger created with __name__ follows the coding guidelines for logging setup.


74-77: LGTM!

The live parameter documentation clearly explains the dry-run default behaviour and the opt-in nature of live publishing.

Also applies to: 99-99


338-352: LGTM!

The shared _format_cargo_failure_message helper eliminates the duplication between packaging and publishing error message construction. The function correctly prefers stderr over stdout for error details.


377-399: LGTM!

The _is_already_published_error implementation addresses the previous review concern about overly broad matching:

  1. Exit code 101 is required (cargo registry error)
  2. Markers are now specific to crates.io rather than generic "already exists"

This combination significantly reduces false-positive risk from unrelated failures.


402-437: LGTM!

The _publish_crates function correctly implements the dry-run vs live flow:

  1. Conditionally includes --dry-run based on live flag
  2. Logs each invocation at INFO level
  3. Continues on already-published crates with a WARNING
  4. Raises PublishPreflightError for unexpected failures

The lazy logging with %s placeholders follows the coding guidelines.


505-510: LGTM!

The integration correctly passes effective_options.live to _publish_crates, ensuring the CLI flag propagates through to the publish execution.

tests/bdd/steps/test_publish_when_steps.py (4)

1-16: LGTM!

The module structure follows the refactored pattern with imports from test_publish_infrastructure. The TYPE_CHECKING guard correctly limits the Path import to type-checking time.


22-33: LGTM!

The preflight check step correctly captures PublishPreflightError and returns it in the result dictionary for assertion in @then steps.


36-63: LGTM!

Both standard and --forbid-dirty variants delegate to _invoke_publish_with_options, maintaining consistency and reducing duplication.


66-89: LGTM!

The live-publish step:

  1. Uses the extracted _is_cargo_publish_command helper as recommended in past review
  2. Ensures a default successful cargo publish override exists if none configured
  3. Passes --live to the invocation helper

This addresses the past review comment about extracting the cargo publish detection logic.

tests/bdd/steps/test_publish_helpers.py (6)

1-14: LGTM!

The module structure correctly uses TYPE_CHECKING to guard imports only needed for type annotations, avoiding runtime import costs.


30-49: LGTM!

_load_staged_manifest composes the parsing helpers cleanly. _get_patch_entries uses the positive conditional form as previously suggested, and _split_names is a clear utility for comma-separated input.


52-100: LGTM!

The invocation retrieval helpers provide consistent error messages when expected cargo commands are missing. Using walrus operator with if invocations := ... is idiomatic for this pattern.


103-131: LGTM!

The _has_contiguous_args function now uses any() as previously suggested. The separate contiguous and non-contiguous variants with a unified _has_ordered_args dispatcher is a clean design.


134-169: LGTM!

The consolidated _assert_invocations_flag_presence with should_contain boolean addresses the PR comment about reducing duplication between assertion functions that differed only by polarity. The wrapper functions _assert_invocations_have_flag and _assert_invocations_lack_flag provide semantic clarity at call sites.


172-183: LGTM!

_assert_crate_order_matches provides clear diagnostic output when order assertions fail, including both observed and expected sequences.

tests/bdd/steps/test_publish_then_steps.py (8)

1-31: LGTM!

Module structure is clean: future annotations enabled, TYPE_CHECKING guard used correctly for _PreflightInvocationRecorder, and helper imports are well-organised. The docstring follows NumPy guidelines succinctly.


44-73: LGTM!

These three step functions are well-structured and correctly delegate to the helpers for manifest loading and patch entry retrieval.


96-98: LGTM!

Clear single-purpose helper that improves readability in the calling code.


101-113: LGTM!

Correctly asserts the presence of target-limiting flags in preflight test invocations.


116-125: LGTM!

Clear iteration with explicit assertion for the absence of --exclude flags.


139-161: LGTM!

The all() inversions from prior review feedback are correctly applied, making the assertions readable and correct.


227-292: LGTM!

Skip-reporting assertions are well-structured. Each parses the output, locates the relevant section header, and validates entries. The pattern is consistent across all variants.


295-357: LGTM!

The README staging assertions correctly verify file existence and content equality. The error-type check in then_publish_preflight_reports_missing_socket properly validates the exception class before inspecting the message.

tests/bdd/steps/test_publish_infrastructure.py (10)

1-28: LGTM!

Module structure is sound. The try/except for CmdMox import provides a clean runtime fallback, and TYPE_CHECKING guards are properly applied for type-only imports.


30-36: LGTM!

Immutable, slotted dataclass adhering to project guidelines for structured data.


39-55: LGTM!

Mutable recorder correctly uses slots=True without frozen since the records list is appended to during test execution.


58-64: LGTM!

Frozen, slotted configuration dataclass correctly models the stub setup.


80-86: LGTM!

Protocol and type alias are well-defined, enabling type-safe handler signatures.


89-108: LGTM!

Argument prefix validation is clear and provides detailed error messages on mismatch.


111-123: LGTM!

Clean delegation to the publish module's normalisation logic for cargo commands.


126-128: LGTM!

Extracted predicate addresses the complex-conditional feedback from prior reviews.


131-151: LGTM!

Handler factory correctly validates arguments, resolves callable or static responses, and records invocations when a recorder is provided.


154-164: LGTM!

Straightforward factory function for stub configuration construction.

Comment thread docs/usage-guide.md
Comment thread tests/bdd/steps/test_publish_infrastructure.py
Comment thread tests/bdd/steps/test_publish_then_steps.py
Comment thread tests/bdd/steps/test_publish_then_steps.py
Comment thread tests/bdd/steps/test_publish_then_steps.py
Comment thread tests/bdd/steps/test_publish_then_steps.py
- Clarify publish.py run() docstring to reflect full publish flow.
- Require PWD in environment for cargo publish pre-flight stub, ensuring stricter validation.
- Add error handling for malformed staging lines in test helpers.
- Make PreflightTestContext dataclass frozen and slot-enabled for better performance and immutability.
- Clean up and simplify BDD test step functions handling publish plans and invocations.
- Minor docs fix for usage-guide.md punctuation.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

…onment handling

- Expand docstring for publish CLI command to better describe pre-flight checks and publishing steps.
- Refactor test_publish_infrastructure to add context manager for managing CMD_MOX_STUB_ENV_VAR environment variable.
- Add notes to _register_preflight_commands explaining behavior of multiple cargo publish overrides.
- Replace manual environment setup/teardown with context manager for clarity and safety in tests.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Define a new exception PublishError inheriting from PublishPreflightError to represent failures occurring during the publish step after preflight checks. Update _publish_crates to raise PublishError instead of PublishPreflightError when cargo publish fails. This improves error semantics and classification.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 4, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai 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.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `tests/unit/publish/test_packaging.py:226-235` </location>
<code_context>
+    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]
+
+
+def test_publish_crates_continue_when_version_already_uploaded(
+    tmp_path: Path, caplog: pytest.LogCaptureFixture
+) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Add a companion test for live mode when a crate version is already uploaded

`test_publish_crates_continue_when_version_already_uploaded` only covers the dry-run path (`live=False`). Since `_publish_crates` is supposed to treat already-published errors the same in live and dry-run modes, add coverage for `live=True` (either via a separate test or parametrization) using the same runner, and assert that both crates are still attempted in order and that the already-published error remains non-fatal. This will guard against the live branch diverging from dry-run behavior.

Suggested implementation:

```python
    publish._publish_crates(plan, preparation, runner=runner, live=True)

    expected_roots = [
        staging_root / crate.root_path.relative_to(plan.workspace_root)
        for crate in plan.publishable
    ]
    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]


def test_publish_crates_continue_when_version_already_uploaded_live(
    plan_and_prep, caplog: pytest.LogCaptureFixture
) -> None:
    plan, preparation = plan_and_prep
    staging_root = preparation.staging_root

    class RecordingRunner:
        def __init__(self) -> None:
            self.calls = []
            self._call_count = 0

        def __call__(self, args, cwd) -> None:
            self._call_count += 1
            self.calls.append((tuple(args), cwd))
            if self._call_count == 1:
                # Simulate an "already uploaded" error for the first crate; this
                # should be treated as non-fatal by _publish_crates even in live mode.
                raise RuntimeError("current package version is already uploaded")

    runner = RecordingRunner()

    # Exercise the live branch; already-uploaded errors should be non-fatal and
    # all crates should still be attempted in order.
    publish._publish_crates(plan, preparation, runner=runner, live=True)

    expected_roots = [
        staging_root / crate.root_path.relative_to(plan.workspace_root)
        for crate in plan.publishable
    ]
    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]



```

1. In your codebase, you likely already have a reusable “recording runner” and/or a helper to simulate the “already uploaded” cargo error in the existing `test_publish_crates_continue_when_version_already_uploaded` (dry-run) test. You may want to:
   - Replace the inner `RecordingRunner` class with that shared helper to keep behavior consistent across tests.
   - Adjust the simulated exception type/message (`RuntimeError("current package version is already uploaded")`) to match whatever `_publish_crates` actually inspects (e.g. a specific exception class or error message substring).
2. If you prefer parametrization instead of a separate test, you can refactor both the existing dry-run test and this new live-mode test into a single `@pytest.mark.parametrize("live", [False, True])` test using the same runner and assertions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/unit/publish/test_packaging.py
Add pytest parameterize decorator to test_publish_crates_continue_when_version_already_uploaded to test both live and dry-run scenarios by passing 'live' argument to _publish_crates. This improves test coverage for different publish modes.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>

@codescene-delta-analysis codescene-delta-analysis 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.

Gates Passed
6 Quality Gates Passed

See analysis details in CodeScene

Absence of Expected Change Pattern

  • lading/lading/cli.py is usually changed with: lading/tests/unit/test_cli.py
  • lading/lading/commands/publish.py is usually changed with: lading/tests/bdd/steps/test_publish_steps.py

Quality Gate Profile: Pay Down Tech Debt
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.

@leynos

leynos commented Dec 4, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `tests/unit/publish/test_packaging.py:226-235` </location>
<code_context>
+    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]
+
+
+def test_publish_crates_continue_when_version_already_uploaded(
+    tmp_path: Path, caplog: pytest.LogCaptureFixture
+) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Add a companion test for live mode when a crate version is already uploaded

`test_publish_crates_continue_when_version_already_uploaded` only covers the dry-run path (`live=False`). Since `_publish_crates` is supposed to treat already-published errors the same in live and dry-run modes, add coverage for `live=True` (either via a separate test or parametrization) using the same runner, and assert that both crates are still attempted in order and that the already-published error remains non-fatal. This will guard against the live branch diverging from dry-run behavior.

Suggested implementation:

```python
    publish._publish_crates(plan, preparation, runner=runner, live=True)

    expected_roots = [
        staging_root / crate.root_path.relative_to(plan.workspace_root)
        for crate in plan.publishable
    ]
    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]


def test_publish_crates_continue_when_version_already_uploaded_live(
    plan_and_prep, caplog: pytest.LogCaptureFixture
) -> None:
    plan, preparation = plan_and_prep
    staging_root = preparation.staging_root

    class RecordingRunner:
        def __init__(self) -> None:
            self.calls = []
            self._call_count = 0

        def __call__(self, args, cwd) -> None:
            self._call_count += 1
            self.calls.append((tuple(args), cwd))
            if self._call_count == 1:
                # Simulate an "already uploaded" error for the first crate; this
                # should be treated as non-fatal by _publish_crates even in live mode.
                raise RuntimeError("current package version is already uploaded")

    runner = RecordingRunner()

    # Exercise the live branch; already-uploaded errors should be non-fatal and
    # all crates should still be attempted in order.
    publish._publish_crates(plan, preparation, runner=runner, live=True)

    expected_roots = [
        staging_root / crate.root_path.relative_to(plan.workspace_root)
        for crate in plan.publishable
    ]
    assert runner.calls == [(("cargo", "publish"), root) for root in expected_roots]



```

1. In your codebase, you likely already have a reusable “recording runner” and/or a helper to simulate the “already uploaded” cargo error in the existing `test_publish_crates_continue_when_version_already_uploaded` (dry-run) test. You may want to:
   - Replace the inner `RecordingRunner` class with that shared helper to keep behavior consistent across tests.
   - Adjust the simulated exception type/message (`RuntimeError("current package version is already uploaded")`) to match whatever `_publish_crates` actually inspects (e.g. a specific exception class or error message substring).
2. If you prefer parametrization instead of a separate test, you can refactor both the existing dry-run test and this new live-mode test into a single `@pytest.mark.parametrize("live", [False, True])` test using the same runner and assertions.
</issue_to_address>

@coderabbitai

coderabbitai Bot commented Dec 4, 2025

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the test file to check if the companion test has been added
cat tests/unit/publish/test_packaging.py

Length of output: 9768


@leynos Yes, this issue has been resolved!

The test test_publish_crates_continue_when_version_already_uploaded (lines 235-267) now uses @pytest.mark.parametrize to cover both dry-run and live modes:

@pytest.mark.parametrize(
    "live",
    [pytest.param(False, id="dry-run"), pytest.param(True, id="live")],
)
def test_publish_crates_continue_when_version_already_uploaded(
    tmp_path: Path, caplog: pytest.LogCaptureFixture, *, live: bool
) -> None:
    """Already-published versions log a warning and continue."""
    # ... test implementation ...
    publish._publish_crates(plan, preparation, runner=runner, live=live)

This implementation:

  • ✅ Tests both live=False (dry-run) and live=True modes
  • ✅ Uses the same runner logic for both cases
  • ✅ Verifies that already-published errors (exit code 101) are non-fatal in both modes
  • ✅ Ensures all crates are attempted in order (["alpha", "beta"])
  • ✅ Confirms warning messages are logged

The parametrised approach (option 2 from the review comment) is cleaner than creating a separate test, as it ensures both modes remain consistent whilst avoiding duplication. The test guards against the live branch diverging from dry-run behaviour, exactly as requested.

@leynos
leynos merged commit ebf1828 into main Dec 4, 2025
4 checks passed
@leynos
leynos deleted the terragon/implement-lading-publish-command-tqvzvs branch December 4, 2025 18:21
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.

1 participant