Skip to content

Add the offline candidate boundary and publication resume for crates.io (#1065) - #1083

Open
leynos wants to merge 14 commits into
issue-1064-implement-crates-io-trusted-publishing-and-registry-byte-verificationfrom
issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification
Open

leynos wants to merge 14 commits into
issue-1064-implement-crates-io-trusted-publishing-and-registry-byte-verificationfrom
issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification

Conversation

@leynos

@leynos leynos commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Completes the crates.io Trusted Publishing path by validating the packaged candidate before it leaves the build job and by making an interrupted publication converge on a rerun.

Closes #1065

  • scripts/crates_io_candidate_cli.py now takes an explicit, mutually exclusive release mode. --tag keeps the publishing contract, where the candidate must record exactly the tag being released. --rehearsal accepts only a tagless candidate, so a pull-request lane can exercise the same boundary. Supplying no mode, or both modes, is a usage error.
  • The package-crate composite action runs that check offline, after packaging and before the artefact upload, selecting --rehearsal for a dry run and --tag for a publishing run. It reads only the local identity record and archive: no token, no curl, no crates.io endpoint.
  • The publish-sequence tests now simulate an interrupted publication: a first absent probe decides to publish, a second identical probe decides to skip, and a byte verification confirms the served archive. The workflow contract requires the skip resolution to exclude the authentication, metadata, and upload steps.
  • The package contract pins the new step's id, ordering, environment, exact command, and offline nature, with a mutation case that proves the contract notices a rehearsal checked as a publishing run.
  • Developers' guide, ADR-0007, and the trusted-packaging ExecPlan record the boundary and the resume path.

No long-lived registry secret is introduced and no registry job gains GitHub release-write authority; the exchange job and its contents: read / id-token: write / crates-io environment contract are unchanged.

Testing

  • make check-fmt
  • make lint-python
  • make typecheck-python
  • make markdownlint (includes make spelling)
  • make nixie
  • make test-workflow-contracts
  • make test-release-publication
  • make test-documentation-contracts
  • make github-actions-lint
  • make doc-coverage
  • make test
  • make lint

Stack

Stacked on #1077 (issue-1064-…), which targets main.

References

🤖 Generated with Claude Code

Summary by Sourcery

Enforce crates.io candidate validation before artifact handoff and make rerun publication converge safely after an interrupted upload.

New Features:

  • Add offline candidate validation to the crate packaging action, supporting explicit rehearsal and publishing modes.
  • Make interrupted crates.io publications resume by skipping repeat uploads and verifying the already-published archive.

Enhancements:

  • Refactor candidate archive parsing and validation errors into dedicated modules.
  • Extend workflow contracts and release tests to enforce candidate validation ordering, offline behavior, release-mode selection, and resume sequencing.

Documentation:

  • Document the offline candidate boundary and interrupted-publication recovery path in the developers guide, ADR, and trusted-packaging execution plan.

Tests:

  • Add coverage for tagless rehearsals, mutually exclusive release modes, interrupted publication recovery, archive verification, and workflow contract mutations.
  • Register the expanded candidate validation modules and metadata tests in the release publication test suite.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Note

CodeRabbit ASCII loading animation

Currently processing new changes in this PR. This may take a few minutes, please wait...

Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: 3459031d-d10e-4984-8e18-b55ce8ac66c1
Commits

Reviewing files that changed from the base of the PR and between f066be4 and ddbb125.

Files selected for processing: 14
  • .github/actions/package-crate/action.yml
  • Makefile
  • docs/adrs/adr-0007-publish-the-crates-io-package-as-netsuke-build.md
  • docs/developers-guide.md
  • docs/execplans/use-trusted-packaging.md
  • scripts/crates_io_candidate_archive.py
  • scripts/crates_io_candidate_cli.py
  • scripts/crates_io_candidate_errors.py
  • scripts/tests/test_crates_io_candidate_cli.py
  • scripts/tests/test_crates_io_candidate_metadata.py
  • scripts/tests/test_crates_io_publish_sequence.py
  • tests/workflow_contracts/release_package_contract_test.py
  • tests/workflow_contracts/release_publication_modules.py
  • tests/workflow_contracts/release_publish_crates_io_sequence_test.py

@sourcery-ai

sourcery-ai Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR completes the trusted crates.io publication boundary by validating package identity and bytes offline before artifact handoff, distinguishing tagless rehearsals from tagged releases, and adding contract-backed resume behavior that skips duplicate publication and verifies the already-served archive.

Sequence diagram for offline candidate validation and resumable publication

sequenceDiagram
    participant Package as package-crate
    participant Candidate as crates_io_candidate_cli.py
    participant Artifact as release-crate-package
    participant Publisher as crates.io publisher
    participant Registry as crates.io

    Package->>Candidate: check --rehearsal or --tag
    Candidate->>Candidate: Validate identity and archive locally
    Package->>Artifact: Upload validated candidate
    Publisher->>Artifact: Download candidate
    Publisher->>Candidate: Recheck candidate
    Publisher->>Registry: Probe version
    Registry-->>Publisher: absent
    Publisher->>Registry: Authenticate and upload archive
    Registry-->>Publisher: Publication response
    Publisher->>Registry: Probe version on rerun
    Registry-->>Publisher: identical
    Publisher->>Registry: Download public archive
    Registry-->>Publisher: Served bytes
    Publisher->>Publisher: Verify archive bytes
Loading

State diagram for crates.io publication resume

stateDiagram-v2
    [*] --> CandidateValidated
    CandidateValidated --> Probe
    Probe --> Publish: absent
    Publish --> Published
    Published --> Probe: interrupted rerun
    Probe --> VerifyExisting: identical
    VerifyExisting --> Completed: bytes match
    Probe --> Conflict: conflict or yanked
    Completed --> [*]
    Conflict --> [*]
Loading

File-Level Changes

Change Details Files
Adds an offline candidate validation boundary to the packaging action, with explicit rehearsal and publishing modes.
  • Require exactly one of --rehearsal or --tag in the candidate CLI.
  • Reject tagged rehearsal candidates and require publishing candidates to match the release tag.
  • Run the check after packaging and before artifact upload using only local inputs, with no credentials or registry access.
  • Pin the step ID, ordering, environment, command, and offline behavior in workflow-contract tests.
.github/actions/package-crate/action.yml
scripts/crates_io_candidate_cli.py
scripts/tests/test_crates_io_candidate_cli.py
tests/workflow_contracts/release_package_contract_test.py
docs/adrs/adr-0007-publish-the-crates-io-package-as-netsuke-build.md
docs/developers-guide.md
docs/execplans/use-trusted-packaging.md
Makes crates.io publication reruns converge after an interrupted upload without republishing.
  • Exercise absent → publish, then identical → skip, followed by archive byte verification.
  • Require the skip workflow sequence to omit authentication, metadata, and upload steps while retaining verification.
  • Document the resume behavior and its security/credential implications.
scripts/tests/test_crates_io_publish_sequence.py
tests/workflow_contracts/release_publish_crates_io_sequence_test.py
docs/adrs/adr-0007-publish-the-crates-io-package-as-netsuke-build.md
docs/developers-guide.md
docs/execplans/use-trusted-packaging.md

Possibly linked issues


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

@leynos
leynos added this pull request to stack #1084 November 2, 2026 22:53
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from 9957f0d to 4d60144 Compare November 2, 2026 23:55
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from bee867c to 9d1f5d9 Compare November 4, 2026 06:21
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from 9d1f5d9 to b0f0499 Compare November 4, 2026 07:50
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from b0f0499 to 02dd50f Compare November 4, 2026 09:03
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please assess the implementation in this PR for completeness and correctness against the original issue:


## Required changes

Implement `publish-crates-io` in `.github/workflows/release.yml` and a matching
composite action. Compose the existing registry policy, adapters, and
`scripts/publish_crates_io_cli.py`.

Consume the packaged candidate identity and archive without rebuilding.
Probe crates.io, publish absent versions, skip verified identical versions,
and fail for conflicting, yanked, or indeterminate observations. Download
registry bytes and verify them against the packaged digest.

Declare the exchange job in `release.yml` itself with `contents: read`,
`id-token: write`, and environment `crates-io`. Keep registry credentials
separate from GitHub release-write authority. Add no long-lived registry secret.

## Acceptance criteria

- [ ] Depend on metadata, package-crate, all platform builds, Windows native
      recipe smoke, and release-admission-canaries.
- [ ] Permit OIDC exchange and registry mutation only for validated publishing
      runs, never pull-request or dry-run lanes.
- [ ] Test absent, identical, conflicting, yanked, malformed, unavailable,
      and downloaded-byte mismatch cases.
- [ ] Verify candidate-bound artefact hand-off and permission separation.
- [ ] Add publishing graph contracts and interrupted-publication retry coverage.

## Context and sources

Requested by @leynos in [this comment](https://github.com/leynos/netsuke/pull/903#issuecomment-6551740869).
Follow up [PR #903](https://github.com/leynos/netsuke/pull/903).
Intended assignee: @leynos. Automatic assignment failed because the integration
cannot perform `replaceActorsForAssignable`.

Evidence revision: `5c2e5734f94df0c95d664573e1abd80e4b7d50c7`. This revision packages the crate and stages
GitHub assets but does not publish to crates.io.

- [Release ADR](https://github.com/leynos/netsuke/blob/5c2e5734f94df0c95d664573e1abd80e4b7d50c7/docs/adrs/adr-0007-publish-the-crates-io-package-as-netsuke-build.md#addendum-2026-10-24-the-trusted-publisher)
- [ExecPlan implementation requirements](https://github.com/leynos/netsuke/blob/5c2e5734f94df0c95d664573e1abd80e4b7d50c7/docs/execplans/use-trusted-packaging.md#stage-c--implementation)
- [Release workflow](https://github.com/leynos/netsuke/blob/5c2e5734f94df0c95d664573e1abd80e4b7d50c7/.github/workflows/release.yml)

Reconcile changes with the current ExecPlan. Update the release runbook and
plan status when implementation and verification are complete.

@coderabbitai

coderabbitai Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Tip

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

Analysis chain

Script executed:

#!/bin/bash
git status --short
gh pr view 1083 --repo leynos/netsuke --json baseRefName,headRefOid,files,statusCheckRollup
fd -H 'AGENTS.md|publish.*crates|crates.*candidate|trusted.packaging|developers|adr-0007|release.*contract|publication' .
ast-grep outline .github/workflows/release.yml
sed -n '1,240p' .github/workflows/release.yml

Length of output: 21482


Script executed:

#!/bin/bash
cat AGENTS.md
sed -n '290,700p' .github/workflows/release.yml
fd -H . .github/actions/publish-crates-io .github/actions/package-crate
ast-grep outline scripts --items all --type function,class --match 'candidate|publish|probe|verify|registry'
cat .github/actions/publish-crates-io/action.yml
cat .github/actions/package-crate/action.yml
git diff HEAD^ --stat
rg -n -C3 '1065|1064|Stage C|publish-crates|interrupted|resume|Trusted|trusted|crates.io|runbook' docs/execplans/use-trusted-packaging.md docs/developers-guide.md docs/adrs/adr-0007-publish-the-crates-io-package-as-netsuke-build.md

Length of output: 201127


Script executed:

#!/bin/bash
gh pr diff 1083 --repo leynos/netsuke -- scripts/crates_io_candidate_cli.py 2>/dev/null || true
for f in scripts/crates_io_candidate_cli.py scripts/crates_io_candidate_archive.py scripts/crates_io_transport_cli.py scripts/crates_io_transport_http.py scripts/crates_io_registry_http.py scripts/release_registry_response.py scripts/publish_crates_io_cli.py scripts/tests/test_crates_io_publish_sequence.py tests/workflow_contracts/release_publish_crates_io_sequence_test.py tests/workflow_contracts/release_publish_crates_io_contracts.py; do
echo "=== $f"
ast-grep outline "$f"
done
cat -n scripts/crates_io_candidate_cli.py
cat -n scripts/crates_io_candidate_archive.py
cat -n scripts/release_registry_response.py
cat -n scripts/crates_io_transport_cli.py
cat -n scripts/crates_io_registry_http.py
cat -n scripts/tests/test_crates_io_publish_sequence.py
cat -n tests/workflow_contracts/release_publish_crates_io_sequence_test.py

Length of output: 81382


Script executed:

#!/bin/bash
for f in scripts/crates_io_upload_metadata.py scripts/release_reconcile_crates.py scripts/release_crates_io_adapter.py tests/workflow_contracts/release_package_contract_test.py; do ast-grep outline "$f"; done
sed -n '130,245p' scripts/publish_crates_io_cli.py
sed -n '120,230p' scripts/release_reconcile_crates.py
cat -n scripts/crates_io_upload_metadata.py
sed -n '180,340p' scripts/release_crates_io_adapter.py
sed -n '1,230p' tests/workflow_contracts/release_package_contract_test.py
fd 'release.*(permissions|crates_io|reachability).*test.py' tests/workflow_contracts
rg -n -C2 'mismatch|different|unavailable|malformed|rehearsal|missing.mode|both' scripts/tests/test_publish_crates_io_cli.py scripts/tests/test_crates_io_candidate_cli.py
rg -n '^\s*(def test_|Status:)|\[.\].*(publisher|registry|crates|Stage C|Stage D|retry|offline)' tests/workflow_contracts/release_publish_crates_io* tests/workflow_contracts/release_permissions_test.py docs/execplans/use-trusted-packaging.md
gh api repos/leynos/netsuke/actions/jobs/126561243632 --jq '.steps[] | [.name,.conclusion] | @tsv'
gh api repos/leynos/netsuke/actions/jobs/126548513567 --jq '.steps[] | [.name,.conclusion] | @tsv'
gh run view 41822355573 --repo leynos/netsuke --job 126561242195 --log-failed | tail -n 65

Length of output: 63793


web_search executed:

Cargo registry web API publish crates/new metadata framing Authorization Bearer token rust-lang crates-io-auth-action token output trusted publishing documentation

Length of output: 19730


Script executed:

#!/bin/bash
fd 'release.*(permission|crates|reachability|guard)' tests/workflow_contracts
sed -n '230,315p' tests/workflow_contracts/release_publish_crates_io_contracts.py
sed -n '230,315p' tests/workflow_contracts/release_package_contract_test.py
rg -n -C3 'PUBLISHER_NEEDS|check_crates_io|pull_request|dry_run|id-token|environment' tests/workflow_contracts/release_publish_path_contract.py tests/workflow_contracts/release_publish_path_publication.py tests/workflow_contracts/release_publish_path_permissions_test.py
sed -n '1590,1640p' docs/execplans/use-trusted-packaging.md
gh run view 41822355573 --repo leynos/netsuke --json headSha,conclusion
gh run view 41822355483 --repo leynos/netsuke --json headSha,conclusion
gh pr diff 1083 --repo leynos/netsuke --name-only

Length of output: 25100


@leynos Treat the quoted issue as complete at the implementation and automated-test level across the stack. Do not treat the full Trusted Publishing release plan as complete.

I assessed head aaa060513c643ea957a8d7667e1964307ccc61c2, including the publisher inherited from #1077. I found no blocking correctness defect in the inspected publisher path.

Acceptance criteria

Requirement Assessment
Required dependencies Met. publish-crates-io requires metadata, package-crate, all three platform-build jobs, Windows native recipe smoke, and release-admission canaries. Its condition requires every prerequisite to succeed.
Publishing-only OIDC and mutation Met in the workflow and contracts. Validated metadata controls job admission. Inside the action, only a publish decision permits authentication, metadata derivation, and upload. PR and dry-run lanes skip the publisher.
Required registry cases Covered. Tests include absent, identical, conflicting, yanked, malformed, unavailable, and downloaded-byte mismatch cases. Classification failures stop publication.
Candidate-bound hand-off Met. The publisher downloads the named current-run artefact. It checks the recorded identity, commit, tag, package name, version, archive selection, packaged manifest, and SHA-256 before probing the registry. It uploads retained bytes without rebuilding.
Permission separation Met in the checked-in configuration. The exchange job lives in release.yml, uses environment crates-io, and has exactly contents: read and id-token: write. GitHub release writing remains separate. No long-lived registry secret is introduced.
Graph contracts and interrupted retry Met. Contracts check dependency reachability, permissions, artefact inputs, action order, and publication guards. Retry tests cover absent → publish, then identical → skip → byte verification. The skip sequence excludes authentication, metadata derivation, and upload.
Runbook and plan reconciliation Met for this stage. The developers’ guide documents same-run recovery. The ExecPlan records delivery while retaining the unfinished stages.

What #1083 completes

Distinguish this PR’s changes from the inherited implementation:

  • Validate before artefact upload. package-crate now runs the local candidate check after packaging.
  • Require an explicit release mode. --tag requires the recorded release tag. --rehearsal accepts only a tagless candidate. Neither mode, or both modes, produces a usage error.
  • Protect the new boundary with negative controls. The package contract rejects a rehearsal incorrectly checked as a publishing candidate.
  • Add explicit recovery coverage. The new tests connect the second, identical observation to upload-free verification.

The retry coverage combines policy/CLI tests with contracts over the actual action guards. It is not a live interrupted-publication test.

Verification and remaining checks

Hosted results for the assessed head confirm that workflow contracts, release-publication tests, lint, typecheck, formatting, documentation checks, and package verification passed. The Release Dry Run also passed. I inspected those results; I did not run repository code in the sandbox.

Before merging, investigate and rerun the failed kani-smoke check. The log shows a 600-second timeout in compile_guard::every_patched_tree_compiles_under_denied_warnings. That failure does not establish a publisher defect, but the overall CI run is not green.

Before the first production publication, verify the external crates-io environment and crates.io Trusted Publisher registration. The supplied execution evidence does not verify those settings or a live OIDC exchange. The dry run correctly skipped the publisher.

Keep the wider ExecPlan IN PROGRESS. Inventory-verification orchestration, GitHub release finalization, and consumer smoke remain open. publish-release still does not wait for crates.io success. The documentation states that limitation explicitly; it is broader-plan work, not a missing requirement in the quoted publisher issue.

You are interacting with an AI system.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from aaa0605 to a0f9f56 Compare November 4, 2026 14:58
codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from a0f9f56 to 8cf0952 Compare November 4, 2026 16:36
codescene-access[bot]

This comment was marked as outdated.

leynos and others added 4 commits November 4, 2026 18:40
The candidate check previously demanded a release tag, so a pull-request
rehearsal had no way to validate the packaged archive: a rehearsal records
no tag, and inventing one would let a pull request claim a release it never
built.

Make the tag optional and select the mode with a required mutually
exclusive group. `--tag` keeps the publishing contract, where the identity
must record exactly the tag being released; `--rehearsal` accepts a
tagless identity and rejects one that carries a tag. Supplying neither or
both is a usage error rather than a check that silently accepts whichever
identity it was handed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A publication can stop after the upload but before the downloaded bytes
are verified. The rerun then observes an identical version, and the
publisher must stand down rather than publish again: crates.io rejects a
second upload of an immutable version, so the skip is what makes the retry
converge.

Drive the publish sequence through that history: an absent probe decides
to publish, a second identical probe decides to skip, and the verification
step then confirms the served bytes match the packaged digest. Pin the
same boundary in the workflow contract by requiring the skip resolution to
exclude the authentication, metadata, and upload steps while retaining
verification.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The publisher revalidates the candidate after the artefact hand-off, but
nothing proved the archive and its identity record agreed while both were
still local. A pull-request lane also had no rehearsal of that boundary
without reaching the registry.

Run the candidate check in the packaging action, immediately after the
archive is produced and before it is uploaded. The step selects
`--rehearsal` for a dry run and `--tag` for a publishing run, mirroring the
packaging step's own mode selector, and carries no token or registry
request. Extend the package contract to pin the step's id, ordering,
environment, exact command, and offline nature, with a mutation case that
proves the contract notices a rehearsal checked as a publishing run.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Record the candidate boundary the packaging action now enforces, the
required release mode that distinguishes a rehearsal from a publishing
run, and the resume path an interrupted publication takes. The developers'
guide, ADR-0007, and the trusted-packaging ExecPlan each name the tests
that hold those behaviours, so a later edit can tell which claim is
pinned and where.

Co-Authored-By: Claude Code <noreply@anthropic.com>
leynos and others added 10 commits November 4, 2026 18:40
`crates_io_candidate_cli` reached 404 lines against the repository's
400-line module limit, and CodeScene reported the identity matcher over its
complexity threshold.

Move the `CandidateIssue`/`CandidateError` taxonomy into
`crates_io_candidate_errors`, leaving each reader to raise a shared
vocabulary without importing the command-line module that composes them.
Extract the release-mode comparison from `_identity_matches` so the identity
and tag comparisons are separately testable.

Collapse the duplicated publishing/rehearsal test helpers into one builder
that derives the mode from the expectation, removing the structural
duplication CodeScene flagged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
`crates_io_candidate_cli` still averaged above CodeScene's mean-complexity
threshold after the error vocabulary moved out. The archive and manifest
readers are a cohesive tar/TOML boundary that the command-line module only
composes, so move them into `crates_io_candidate_archive`.

Both modules now score 10.0 in CodeScene's file review, and the packaging
manifest is validated by a unit that can be tested without the CLI.

Co-Authored-By: Claude Code <noreply@anthropic.com>
"Supplying neither mode, or both" pairs `neither` with `or`, which is
ungrammatical. Say "no mode, or both modes" in the developers' guide and the
trusted-packaging ExecPlan.

Co-Authored-By: Claude Code <noreply@anthropic.com>
`archive_and_crate` and `read_packaged_manifest` are the module's public
surface, so give each a structured docstring naming its parameters, return
value, and the candidate rejections it raises.

`pathlib` stays in the type-checking block: the module runs on the Python 3.14
baseline, where annotations are deferred, and Ruff's TC003 requires the import
to be type-only because no runtime code uses it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The metadata-mode tests and the check-mode tests were sharing one module.
The base branch's observability work added metadata coverage to it, taking
the file to 422 lines and tripping Pylint's C0302 cap at 400.

Move the metadata tests into ``test_crates_io_candidate_metadata.py``, so
each module states one command's contract and both stay under the cap. The
Makefile recipe, the workflow-contract module list, and the developers'
guide all name the new module, keeping the target's declared and executed
module sets equal.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The offline candidate contract asserted only that both release-mode
assignments appeared somewhere in the step's shell. A swapped mapping
would have passed: a dry run would forward `--tag ""` and a publishing
run would forward `--rehearsal`, failing only at runtime.

Resolve each assignment from its own `case` arm line and compare the
whole assignment, so the arm label and the mode it selects are bound
together. The existing mutation case still replaces the rehearsal
assignment and is still rejected.

Co-Authored-By: Claude Code <noreply@anthropic.com>
CodeRabbit flagged three trivial style issues in the candidate boundary.
The private helpers in the candidate CLI and the package contract test
carried multi-paragraph docstrings with structured sections, which the
project's pydoclint configuration exempts for one-line private helpers;
their rationale now sits in a comment beside the code. The archive
reader's two `isinstance` branches now use structural pattern matching,
which the repository's Python typing rules prefer for branching over
structured data. Error classification is unchanged: a non-string or
path-shaped crate name still raises CRATE_BASENAME, a missing or
non-mapping package table still raises PACKAGE_TABLE, and a mapping that
names another package still raises MANIFEST_IDENTITY.

Co-Authored-By: Claude Code <noreply@anthropic.com>
CodeRabbit noticed that the new archive adapter was missing from the
release-publication inventory: `SOURCE_MODULES` drives both the
`--doctest-modules` command line and `NETWORK_FREE_MODULES`, so the
module's tar and TOML boundary was unchecked. List it in
`SOURCE_MODULES`, `ADAPTER_MODULES`, and the Makefile recipe, and give
`archive_and_crate` a doctest example so the collected doctest count is
not vacuous.

Widen the package contract's offline denylist to cover credential
references, other fetch tools, and registry URLs anywhere in the step,
not only in the tokenized `uv run` command. The assertion remains a
denylist, not a proof, so the ADR and ExecPlan now describe it as a
check on the step's shell text rather than a guarantee that the step
reaches no registry endpoint.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The candidate error module was the one release-publication source the
boundary contracts did not name, so a later edit that gave it file or
network effects would have passed unnoticed. List it in
`SOURCE_MODULES` and `POLICY_MODULES`, wire it into the
`test-release-publication` doctest command, and give `CandidateError` an
example so the collected doctest count stays non-zero. The module only
formats classified issues, so the policy-side classification holds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The ExecPlan's progress log stopped at the publisher telemetry work, so
the offline candidate boundary and the interrupted-publication resume
path that issue #1065 delivers were absent from the plan's authoritative
record. Add one entry naming the release-mode contract, the archive and
error modules and their registration, the offline step in the
`package-crate` action, the resumed publication sequence, and the gate
and review evidence for the head. The entry also restates that the
finalization, inventory-verification, and consumer-smoke stages remain
open, so the plan does not read as an end-to-end release path.

Gates: check-fmt, markdownlint (with spelling), and nixie.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@leynos
leynos force-pushed the issue-1065-implement-crates-io-trusted-publishing-and-registry-byte-verification branch from 8cf0952 to ddbb125 Compare November 4, 2026 18:46
@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai please assess the implementation in this PR for completeness and correctness against the execplan:

docs/execplans/use-trusted-packaging.md

Published head: ddbb125; comparison base: f066be4.

This is a candidate-bound follow-up to your earlier assessment of head aaa060513c643ea957a8d7667e1964307ccc61c2 (request #issuecomment-6631147159, reply #issuecomment-6631198831). The branch has been rebased twice since then, onto the advanced base branch. The fourteen commits that belong to this pull request replay patch-identically: git range-diff reports = for every one, and the tree delta against the previous head is byte-identical to the inherited base delta. No implementation content changed in the rebases.

Acceptance evidence, local, sequential, one gate at a time, on the published head with a clean working tree:

  • make check-fmt 0; make lint-clippy 0; make lint-whitaker 0; make lint-python 0 (Pylint 10.00/10, interrogate 100%); make github-actions-lint 0
  • make doc-coverage 0 (98.30% against the 80% threshold)
  • make test-nextest 0 (5071 passed, 7 skipped); make doctest 0 (168 passed); make typecheck 0
  • make test-workflow-contracts 0 (1721 passed, 3 skipped; 28 passed); make test-release-publication 0 (599 passed); make test-documentation-contracts 0 (15 passed, 48 skipped)
  • make markdownlint 0 (215 files); make spelling 0 (current: typos.toml); make nixie 0 (32 diagrams)

Hosted evidence on the immediately preceding head a0f9f565cc83f6ef3e1a5846459b24a0a28611cb, whose tree differs only by the inherited base work: every applicable check green, including release / package-crate, which runs the offline candidate rehearsal, and release / release-admission-canaries. release / publish-crates-io and release / publish-release were skipped on the pull-request lane, as the gating requires. The hosted checks for this head are still running.

Known limitations: no live OIDC exchange, registry upload, or release finalisation has been performed by this branch; the crates-io environment and the crate's Trusted Publisher registration must be verified before the first production publication. Stage C inventory verification and GitHub finalisation, the consumer smoke test, and the Stage D gating of publish-release on the publisher remain open in the wider plan and outside the scope of issue #1065. This pull request introduces no Kani, Verus, CrossHair, or LemmaScript proof, so the separate proof assessment is recorded as not applicable.

Assessment-ID: utp-1065-ddbb1258-20261104

Please echo the Assessment-ID and assessed base/head in your substantive reply.
This is an assessment conversation, not a request to start a formal PR review.

Identify remaining in-scope omissions or correctness defects and any access or inspection limitations. Assess the implementation, not just the plan text.

@coderabbitai

coderabbitai Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Tip

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

Analysis chain

Script executed:

#!/bin/bash
git status --short
git rev-parse HEAD
git cat-file -t ddbb1258420cfefde87ed14d3071907c6eb121a7
git cat-file -t f066be4f34499f2c24ff8939e95428b95708c836
fd -H '^AGENTS\.md$' .
cat AGENTS.md
git diff --stat f066be4f34499f2c24ff8939e95428b95708c836 ddbb1258420cfefde87ed14d3071907c6eb121a7
git diff --name-only f066be4f34499f2c24ff8939e95428b95708c836 ddbb1258420cfefde87ed14d3071907c6eb121a7
ast-grep outline docs/execplans/use-trusted-packaging.md
rg -n -C4 '^## |^### |1065|1064|offline|interrupted|resume|IN PROGRESS|Status:|publish-crates-io' docs/execplans/use-trusted-packaging.md
gh pr view 1083 --repo leynos/netsuke --json headRefOid,baseRefOid,statusCheckRollup

Length of output: 68088


Script executed:

#!/bin/bash
gh api repos/leynos/netsuke/compare/f066be4f34499f2c24ff8939e95428b95708c836...ddbb1258420cfefde87ed14d3071907c6eb121a7 --jq '{status, ahead_by, total_commits, files:[.files[]|{filename,status,additions,deletions,patch}]}'
sed -n '246,279p;418,480p;640,680p;797,830p;899,930p;1010,1061p;2666,2692p' docs/execplans/use-trusted-packaging.md
ast-grep outline .github/actions/package-crate/action.yml
ast-grep outline .github/actions/publish-crates-io/action.yml
cat -n .github/actions/package-crate/action.yml
cat -n .github/actions/publish-crates-io/action.yml
ast-grep outline scripts/crates_io_candidate_cli.py
ast-grep outline scripts/crates_io_candidate_archive.py
ast-grep outline scripts/tests/test_crates_io_publish_sequence.py
ast-grep outline tests/workflow_contracts/release_publish_crates_io_sequence_test.py
cat -n scripts/crates_io_candidate_cli.py
cat -n scripts/crates_io_candidate_archive.py
cat -n scripts/tests/test_crates_io_publish_sequence.py
cat -n tests/workflow_contracts/release_publish_crates_io_sequence_test.py

Length of output: 145906


Script executed:

#!/bin/bash
ast-grep outline .github/workflows/release.yml
rg -n -C3 'package-crate:|publish-crates-io:|publish-release:|Upload the packaged|Package the crate|persist-credentials|environment: crates-io|should_publish|dry_run' .github/workflows/release.yml
sed -n '295,490p' .github/workflows/release.yml
ast-grep outline tests/workflow_contracts/release_package_contract_test.py
sed -n '90,120p;180,285p;320,390p' tests/workflow_contracts/release_package_contract_test.py
ast-grep outline scripts/tests/test_crates_io_candidate_cli.py
sed -n '175,306p' scripts/tests/test_crates_io_candidate_cli.py
ast-grep outline scripts/tests/test_crates_io_candidate_guards.py
ast-grep outline tests/workflow_contracts/release_publication_modules.py
sed -n '1,145p' tests/workflow_contracts/release_publication_modules.py
ast-grep outline scripts/publish_crates_io_cli.py
sed -n '135,245p' scripts/publish_crates_io_cli.py
rg -n -C2 'mismatch|unavailable|malformed' scripts/tests/test_publish_crates_io_cli.py
rg -n -C2 'test-release-publication|test-release-negative-mutation|github-actions-lint|bash -n|yamllint.*actions' Makefile .github/workflows/ci.yml
gh pr view 1083 --repo leynos/netsuke --json headRefOid,statusCheckRollup --jq '{headRefOid,checks:[.statusCheckRollup[]|{name,status,conclusion,detailsUrl}]}'

Length of output: 51642


@buzzybee-df12 Treat the issue #1065 implementation as complete within its stated scope. Keep the full ExecPlan IN PROGRESS. I found no remaining in-scope omission or blocking correctness defect in the inspected implementation.

  • Assessment-ID: utp-1065-ddbb1258-20261104
  • Assessed base: f066be4f34499f2c24ff8939e95428b95708c836
  • Assessed head: ddbb1258420cfefde87ed14d3071907c6eb121a7

Implementation assessment

I inspected the current-head code, the exact GitHub base/head comparison, the relevant tests, and the ExecPlan requirements. The comparison reports 14 commits and 13 changed files. The publisher action and release workflow are inherited implementation, not changes introduced by this follow-up.

Requirement Assessment
Validate before artefact hand-off Met. .github/actions/package-crate/action.yml, Line 100, adds validation after packaging. The workflow uploads the archive only after the composite action succeeds. Strict shell handling stops the action when validation fails.
Select an explicit release mode Met. The parser requires exactly one of --tag and --rehearsal. Rehearsal rejects a tagged identity. Publishing rejects a tagless identity and requires the exact expected tag.
Bind the candidate to retained bytes Met. Validation checks commit, package name, version, archive selection, regular-file status, recorded SHA-256, and packaged manifest identity. It neither rebuilds nor rewrites the archive. The archive reader does not extract archive members.
Keep rehearsal validation local Met for the candidate check. The new invocation uses local identity and archive paths. The inspected validation path contains no registry request or token use. The shell denylist and network-boundary contracts support this conclusion; the denylist alone is not a proof of network isolation.
Resume interrupted publication Met at policy/CLI and workflow-contract levels. Tests connect absent → publish, then identical → skip, then successful byte verification. Contracts resolve the actual action guards and exclude authentication, metadata derivation, and upload from the skip sequence.
Preserve publication controls Preserved. The publisher requires seven successful prerequisites and validated publishing metadata. Its job declares environment crates-io, contents: read, and id-token: write. The action exchanges credentials only for publish. GitHub release-write authority remains separate.
Preserve test collection and documentation Met. The extracted source modules and moved metadata tests appear in the explicit module inventory and Makefile recipe. CI invokes make test-release-publication. The runbook, ADR, and ExecPlan describe the delivered boundary and recovery path.

The rejection coverage includes ambiguous release modes, identity mismatches, path-shaped archive names, multiple archives, symlinked archives, digest mismatches, and packaged-manifest mismatches. Registry coverage retains conflict, yanked, malformed, unavailable, and downloaded-byte mismatch cases.

Keep these completion boundaries explicit

Do not describe the retry tests as a live interrupted publication. The tests supply recorded registry responses and local downloaded bytes. Separate contracts check the action sequence. They do not execute an upload and then interrupt a running GitHub Actions job.

Keep the following wider-plan work open:

  • Inventory-verification orchestration and same-run operating-system package provenance.
  • GitHub release finalization and consumer smoke.
  • Gating the completed GitHub release path on the publisher outcome.
  • The separate negative-mutation target.
  • The composite-action YAML and shell-syntax lint target, plus its CI wiring.
  • The EP-M4 package archive file-list work recorded in the plan.

These gaps prevent full ExecPlan completion. They do not establish a missing requirement in this candidate-bound follow-up.

Verification and inspection limits

  • Current-head CI is not yet established as green. The final snapshot still shows queued or running checks, including kani-smoke. No completed failure appears in that snapshot. Wait for the required checks on this exact head before merging. Do not carry the earlier head’s Kani failure forward as a current defect.
  • Treat the supplied local gate results as author-reported evidence. I did not rerun builds, tests, or repository code in the sandbox.
  • The local clone lacks the base commit object. GitHub’s comparison API supplied the exact requested comparison. I did not independently reproduce the historical git range-diff or byte-identical rebase claim.
  • Live publication remains unverified. Before the first production publication, verify the GitHub crates-io environment and crates.io Trusted Publisher registration. Then verify the live OIDC exchange, upload, and downloaded registry bytes through the authorized release process.
  • Record new-proof assessment as not applicable. This delta introduces no Rust source or new formal proof.

Retain the scoped completion claim, not an end-to-end release-readiness claim. No formal PR review or approval action was requested or submitted.

You are interacting with an AI system.

@leynos
leynos marked this pull request as ready for review November 4, 2026 19:51
@sourcery-ai

sourcery-ai Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Sorry @leynos, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 day and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T19:53:11.928977Z ddbb125 Draft marked ready
🔒 Security Review 🔄 Running since 2026-11-04T19:52:02.543728Z ddbb125 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch has not been deployed

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

Implement crates.io Trusted Publishing and registry byte verification

2 participants