mpatch 1.7.0__tar.gz → 1.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/ci.yml +5 -5
  2. mpatch-1.8.0/.github/workflows/crates.yml +59 -0
  3. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/docs.yml +5 -3
  4. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/pypi.yml +2 -2
  5. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/python-ci.yml +3 -3
  6. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/release.yml +14 -7
  7. {mpatch-1.7.0 → mpatch-1.8.0}/CHANGELOG.md +12 -1
  8. {mpatch-1.7.0 → mpatch-1.8.0}/Cargo.lock +2 -2
  9. {mpatch-1.7.0 → mpatch-1.8.0}/Cargo.toml +1 -1
  10. {mpatch-1.7.0 → mpatch-1.8.0}/PKG-INFO +1 -1
  11. {mpatch-1.7.0 → mpatch-1.8.0}/README.md +3 -3
  12. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/Cargo.toml +1 -1
  13. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/mpatch.pyi +2 -2
  14. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/src/lib.rs +2 -2
  15. {mpatch-1.7.0 → mpatch-1.8.0}/pyproject.toml +1 -1
  16. {mpatch-1.7.0 → mpatch-1.8.0}/scripts/release.py +113 -69
  17. {mpatch-1.7.0 → mpatch-1.8.0}/src/lib.rs +667 -43
  18. {mpatch-1.7.0 → mpatch-1.8.0}/src/main.rs +266 -1
  19. {mpatch-1.7.0 → mpatch-1.8.0}/tests/patch_apply_tests.rs +1211 -312
  20. {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/test-report.yml +0 -0
  21. {mpatch-1.7.0 → mpatch-1.8.0}/.gitignore +0 -0
  22. {mpatch-1.7.0 → mpatch-1.8.0}/LICENSE +0 -0
  23. {mpatch-1.7.0 → mpatch-1.8.0}/benches/mpatch_bench.rs +0 -0
  24. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/.gitignore +0 -0
  25. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/README.md +0 -0
  26. {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/tests/test_mpatch.py +0 -0
@@ -21,10 +21,10 @@ jobs:
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
23
  - name: Checkout repository
24
- uses: actions/checkout@v4
24
+ uses: actions/checkout@v7
25
25
 
26
26
  - name: Install Rust toolchain
27
- uses: actions-rust-lang/setup-rust-toolchain@v1
27
+ uses: actions-rust-lang/setup-rust-toolchain@v2
28
28
  with:
29
29
  toolchain: stable
30
30
  components: rustfmt
@@ -43,7 +43,7 @@ jobs:
43
43
 
44
44
  steps:
45
45
  - name: Checkout repository
46
- uses: actions/checkout@v4
46
+ uses: actions/checkout@v7
47
47
  with: { fetch-depth: 0 }
48
48
 
49
49
  # --- Smart Change Detection ---
@@ -60,7 +60,7 @@ jobs:
60
60
 
61
61
  - name: Get changed files
62
62
  id: changed_files
63
- uses: tj-actions/changed-files@v45
63
+ uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46
64
64
  with:
65
65
  base_sha: ${{ steps.last_successful_commit.outputs.base }}
66
66
  files_yaml: |
@@ -74,7 +74,7 @@ jobs:
74
74
  # --- Setup ---
75
75
  - name: Install Rust toolchain
76
76
  if: steps.changed_files.outputs.code_any_changed == 'true'
77
- uses: actions-rust-lang/setup-rust-toolchain@v1
77
+ uses: actions-rust-lang/setup-rust-toolchain@v2
78
78
  with:
79
79
  toolchain: stable
80
80
  cache: true
@@ -0,0 +1,59 @@
1
+ name: Publish Rust Crate to crates.io
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ env:
12
+ CARGO_TERM_COLOR: always
13
+
14
+ jobs:
15
+ publish-crate:
16
+ name: Upload release to crates.io
17
+ runs-on: ubuntu-latest
18
+
19
+ environment:
20
+ name: crates-io
21
+ url: https://crates.io/crates/mpatch
22
+
23
+ permissions:
24
+ id-token: write
25
+ contents: read
26
+
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v7
30
+
31
+ - name: Install Rust toolchain
32
+ uses: actions-rust-lang/setup-rust-toolchain@v2
33
+ with:
34
+ toolchain: stable
35
+ cache: false
36
+
37
+ # Refuse to publish if the release tag and the crate version disagree.
38
+ - name: Check Tag Matches Crate Version
39
+ if: github.event_name == 'release'
40
+ shell: bash
41
+ env:
42
+ REF_NAME: ${{ github.ref_name }}
43
+ run: |
44
+ CRATE_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "mpatch") | .version')
45
+ if [[ "${REF_NAME#v}" != "$CRATE_VERSION" ]]; then
46
+ echo "Error: Tag '$REF_NAME' does not match crate version '$CRATE_VERSION'"
47
+ exit 1
48
+ fi
49
+
50
+ # Trusted Publishing: exchanges the workflow's OIDC token for a short-lived
51
+ # crates.io token, so no long-lived CARGO_REGISTRY_TOKEN secret is needed.
52
+ - name: Authenticate with crates.io
53
+ id: auth
54
+ uses: rust-lang/crates-io-auth-action@v1
55
+
56
+ - name: Publish crate
57
+ run: cargo publish -p mpatch --locked
58
+ env:
59
+ CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - name: Checkout repository
21
- uses: actions/checkout@v4
21
+ uses: actions/checkout@v7
22
22
  with:
23
23
  # Required for tj-actions/changed-files to diff against the correct base.
24
24
  fetch-depth: 0
@@ -36,10 +36,12 @@ jobs:
36
36
  - .github/workflows/docs.yml
37
37
 
38
38
  - name: Install Rust toolchain
39
- uses: actions-rust-lang/setup-rust-toolchain@v1
39
+ uses: actions-rust-lang/setup-rust-toolchain@v2
40
40
  with:
41
41
  toolchain: stable
42
- cache: 'cargo'
42
+ cache: true
43
+ # sccache (below) caches compiled crates, so only cache the cargo registry here.
44
+ cache-targets: false
43
45
 
44
46
  - name: Setup sccache
45
47
  if: steps.changed_files.outputs.docs_relevant_any_changed == 'true'
@@ -31,7 +31,7 @@ jobs:
31
31
  - { os: windows-latest, target: aarch64-pc-windows-msvc }
32
32
 
33
33
  steps:
34
- - uses: actions/checkout@v4
34
+ - uses: actions/checkout@v7
35
35
 
36
36
  - name: Set up QEMU
37
37
  if: runner.os == 'Linux'
@@ -64,7 +64,7 @@ jobs:
64
64
  name: Build source distribution
65
65
  runs-on: ubuntu-latest
66
66
  steps:
67
- - uses: actions/checkout@v4
67
+ - uses: actions/checkout@v7
68
68
 
69
69
  - name: Build sdist
70
70
  uses: PyO3/maturin-action@v1
@@ -31,7 +31,7 @@ jobs:
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
33
  - name: Checkout repository
34
- uses: actions/checkout@v4
34
+ uses: actions/checkout@v7
35
35
 
36
36
  - name: Setup Python
37
37
  uses: actions/setup-python@v6
@@ -63,10 +63,10 @@ jobs:
63
63
 
64
64
  steps:
65
65
  - name: Checkout repository
66
- uses: actions/checkout@v4
66
+ uses: actions/checkout@v7
67
67
 
68
68
  - name: Install Rust toolchain
69
- uses: actions-rust-lang/setup-rust-toolchain@v1
69
+ uses: actions-rust-lang/setup-rust-toolchain@v2
70
70
  with:
71
71
  toolchain: stable
72
72
  cache: true
@@ -67,14 +67,21 @@ jobs:
67
67
 
68
68
  steps:
69
69
  - name: Checkout repository
70
- uses: actions/checkout@v4
70
+ uses: actions/checkout@v7
71
71
 
72
72
  - name: Install Rust toolchain
73
- uses: actions-rust-lang/setup-rust-toolchain@v1
73
+ uses: actions-rust-lang/setup-rust-toolchain@v2
74
74
  with:
75
75
  toolchain: stable
76
76
  target: ${{ matrix.target }}
77
- cache: 'cargo'
77
+ # Caches saved from a tag build can't be restored by later releases,
78
+ # so caching here would only use up the repository's cache storage.
79
+ cache: false
80
+ # v2 defaults to 'deny', which fails on any warning. cross's
81
+ # aarch64-unknown-linux-musl image wraps the linker in a 'set -x'
82
+ # script, and rustc reports that stderr output as a warning.
83
+ # Lints are enforced by CI, not by release builds.
84
+ build-warnings: warn
78
85
 
79
86
  # --- Auto-detect Binary Name ---
80
87
  - name: Get Binary Name
@@ -222,14 +229,14 @@ jobs:
222
229
  runs-on: macos-latest
223
230
  steps:
224
231
  - name: Checkout
225
- uses: actions/checkout@v4
232
+ uses: actions/checkout@v7
226
233
 
227
234
  # We need Rust installed here just to run 'cargo metadata' to get the name.
228
235
  - name: Install Rust toolchain
229
- uses: actions-rust-lang/setup-rust-toolchain@v1
236
+ uses: actions-rust-lang/setup-rust-toolchain@v2
230
237
  with:
231
238
  toolchain: stable
232
- cache: 'cargo'
239
+ cache: false
233
240
 
234
241
  # --- Auto-detect Binary Name ---
235
242
  - name: Get Binary Name
@@ -345,7 +352,7 @@ jobs:
345
352
  runs-on: ubuntu-latest
346
353
  steps:
347
354
  - name: Checkout
348
- uses: actions/checkout@v4
355
+ uses: actions/checkout@v7
349
356
  with:
350
357
  # Fetch full history so we can find the previous tag for comparison links
351
358
  fetch-depth: 0
@@ -7,9 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.8.0] - 2026-10-24
11
+
12
+ ### Added
13
+
14
+ - **Patch Target Paths:** Resolve targets the way `patch -p1` does for directory-tree diffs (`diff -Naur pkg.orig pkg`, including created and deleted files) and for backup-file diffs (`diff -u file.orig file`). When a target is missing but exists without its leading directory, patch that file and print a warning.
15
+ - **Already-Applied Hunks:** Skip a hunk with a warning when it can only be placed by fuzzy matching and its result is already in the file, so re-applying a patch no longer duplicates its changes. Hunks whose context still matches exactly always apply, so intentional duplicates keep working. Hunks that only delete lines, and hunks whose context still matches exactly after they were applied (such as appends at the end of a list), are not detected.
16
+ - **Git Executable Mode:** Apply Git mode changes between `100644` and `100755` on Unix after the content is written, including created files (`new file mode 100755`), mode-only patches, `-R`, and `--dry-run`. A mode change for a file that neither exists nor is created by the patch fails before anything is written, and `--atomic` skips modes when the batch is discarded. Library callers can read the changes with `parse_git_mode_changes()`.
17
+
10
18
  ### Fixed
11
19
 
12
- - **Python Release Workflow:** Use Python 3.14t for free-threaded wheels, which is the minimum supported version for free-threaded interpreters in Maturin, and assign musllinux tags to musl builds so Linux ABI variants do not collide during artifact merging.
20
+ - **Unified Diff Parsing:** Recognize `diff -Naur` headers, strip timestamps from `---` / `+++` file headers (including timestamps whose tab separator was expanded to spaces), decode Git C-quoted paths, and distinguish file headers from hunk content that begins with `---` or `+++`.
21
+ - **Line Endings:** Preserve CRLF and mixed line endings when patching a file. Previously the whole file was rewritten with LF endings; now unchanged lines keep their own ending and added lines use the file's most common one.
22
+ - **Final Newline:** Keep a file's final newline unless the patch changes its last line. Previously any hunk that reached the end of the file applied the patch's `` state, so a hunk ending in unchanged context could add or remove the final newline. Now a line kept from the file keeps its own newline and a line added by the patch follows the patch's marker, matching GNU `patch`.
23
+ - **CLI Failure Reporting:** Exit with an error instead of succeeding silently when the input contains context diffs (`diff -c`), Git renames or copies, Git binary patches, or symlink and submodule modes. Warn when a Git executable-mode change is not applied.
13
24
 
14
25
  ## [1.7.0] - 2026-10-12
15
26
 
@@ -694,7 +694,7 @@ dependencies = [
694
694
 
695
695
  [[package]]
696
696
  name = "mpatch"
697
- version = "1.7.0"
697
+ version = "1.8.0"
698
698
  dependencies = [
699
699
  "anyhow",
700
700
  "arboard",
@@ -712,7 +712,7 @@ dependencies = [
712
712
 
713
713
  [[package]]
714
714
  name = "mpatch_py"
715
- version = "1.7.0"
715
+ version = "1.8.0"
716
716
  dependencies = [
717
717
  "mpatch",
718
718
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "mpatch"
3
- version = "1.7.0"
3
+ version = "1.8.0"
4
4
  edition = "2021"
5
5
  description = "A smart, context-aware patch tool that applies diffs using fuzzy matching, ideal for AI-generated code."
6
6
  authors = ["Romelium <author@romelium.com>"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpatch
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI Status](https://img.shields.io/github/actions/workflow/status/romelium/mpatch/ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white)](https://github.com/romelium/mpatch/actions/workflows/ci.yml)
4
4
  [![Latest Release](https://img.shields.io/github/v/release/romelium/mpatch?style=flat-square&logo=github&logoColor=white)](https://github.com/romelium/mpatch/releases/latest)
5
- [![Crates.io](https://img.shields.io/crates/v/mpatch?style=flat-square&logo=rust&logoColor=white)](https://crates.io/crates/mpatch)
5
+ [![Crates.io](https://img.shields.io/crates/v/mpatch?style=flat-square&logo=rust&logoColor=white&cacheSeconds=300)](https://crates.io/crates/mpatch)
6
6
  [![License: MIT](https://img.shields.io/crates/l/mpatch)](https://opensource.org/licenses/MIT)
7
7
  [![Downloads](https://img.shields.io/crates/d/mpatch?style=flat-square)](https://crates.io/crates/mpatch)
8
8
 
@@ -102,7 +102,7 @@ The integrity of all released binaries is verified with GPG. The `.sig` signatur
102
102
  2. **Verify the archive:**
103
103
  ```bash
104
104
  # Example for Linux x64
105
- gpg --verify mpatch-x86_64-unknown-linux-gnu-v1.7.0.tar.gz.sig mpatch-x86_64-unknown-linux-gnu-v1.7.0.tar.gz
105
+ gpg --verify mpatch-x86_64-unknown-linux-gnu-v1.8.0.tar.gz.sig mpatch-x86_64-unknown-linux-gnu-v1.8.0.tar.gz
106
106
  ```
107
107
 
108
108
  ### Option 2: Build from Source
@@ -201,7 +201,7 @@ mpatch -vvvv changes.md ./src
201
201
  Add to `Cargo.toml`:
202
202
  ```toml
203
203
  [dependencies]
204
- mpatch = "1.7.0"
204
+ mpatch = "1.8.0"
205
205
  ```
206
206
 
207
207
  ### 1. Simple One-Shot (String to String)
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "mpatch_py"
3
- version = "1.7.0"
3
+ version = "1.8.0"
4
4
  edition = "2021"
5
5
  description = "A smart, context-aware patch tool that applies diffs using fuzzy matching, ideal for AI-generated code."
6
6
  authors = ["Romelium <author@romelium.com>"]
@@ -132,7 +132,7 @@ class Patch:
132
132
  Args:
133
133
  file_path (str | os.PathLike): The relative path of the file to be patched.
134
134
  hunks (list[Hunk]): A list of hunks to be applied to the file.
135
- ends_with_newline (bool, optional): Indicates whether the file should end with a newline. Defaults to True.
135
+ ends_with_newline (bool, optional): Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file. Defaults to True.
136
136
  """
137
137
  ...
138
138
  @classmethod
@@ -179,7 +179,7 @@ class Patch:
179
179
  ...
180
180
  @property
181
181
  def ends_with_newline(self) -> bool:
182
- """Indicates whether the file should end with a newline."""
182
+ """Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file."""
183
183
  ...
184
184
  @ends_with_newline.setter
185
185
  def ends_with_newline(self, ends_with_newline: bool) -> None:
@@ -296,7 +296,7 @@ impl PyPatch {
296
296
  /// Args:
297
297
  /// file_path (str | os.PathLike): The relative path of the file to be patched.
298
298
  /// hunks (list[Hunk]): A list of hunks to be applied to the file.
299
- /// ends_with_newline (bool, optional): Indicates whether the file should end with a newline. Defaults to True.
299
+ /// ends_with_newline (bool, optional): Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file. Defaults to True.
300
300
  #[new]
301
301
  #[pyo3(signature = (file_path, hunks, *, ends_with_newline=true))]
302
302
  fn py_new(file_path: PathBuf, hunks: Vec<PyHunk>, ends_with_newline: bool) -> Self {
@@ -377,7 +377,7 @@ impl PyPatch {
377
377
  }
378
378
 
379
379
  #[getter]
380
- /// Indicates whether the file should end with a newline.
380
+ /// Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file.
381
381
  fn ends_with_newline(&self) -> bool {
382
382
  self.inner.ends_with_newline
383
383
  }
@@ -5,7 +5,7 @@ build-backend = "maturin"
5
5
  [project]
6
6
  name = "mpatch"
7
7
  # Keep this in sync with your Rust crate version
8
- version = "1.7.0"
8
+ version = "1.8.0"
9
9
  description = "A smart, context-aware patch tool that applies diffs using fuzzy matching, ideal for AI-generated code."
10
10
  readme = "README.md"
11
11
  authors = [
@@ -1,4 +1,102 @@
1
1
  #!/usr/bin/env python3
2
+ """Release orchestrator for mpatch.
3
+
4
+ Verifies the build, bumps the version, and commits, tags and pushes a
5
+ release. Pushing the tag starts the GitHub workflows that build and
6
+ publish everything else.
7
+
8
+ Usage
9
+ -----
10
+ python scripts/release.py <level> [options]
11
+
12
+ <level> is 'patch', 'minor', 'major', or an explicit version such as
13
+ '1.8.0' (it must be greater than the current version). Choose it from
14
+ the [Unreleased] section of CHANGELOG.md: only "Fixed" entries -> patch,
15
+ any "Added" entries -> minor, breaking changes -> major.
16
+
17
+ -n, --dry-run Show what would change without touching files or git
18
+ (the checks and tests still run).
19
+ -y, --yes Answer yes to every prompt; also pushes unless
20
+ --no-push is given.
21
+ --push / --no-push Push the commit and tag without asking / don't push.
22
+ --skip-tests Skip fmt, clippy, doc, package, test, ruff and pytest.
23
+ --skip-git-check Skip the branch, clean-tree, remote and tag checks.
24
+
25
+ One-time setup
26
+ --------------
27
+ Local tools (the script checks for these before every release):
28
+ - git, with push access to the repository and an identity set:
29
+ `git config --global user.name "Your Name"` and
30
+ `git config --global user.email "you@example.com"`
31
+ - Rust stable from https://rustup.rs, plus
32
+ `rustup component add rustfmt clippy`
33
+ - Python 3 with ruff: `pip install ruff`
34
+ - Optional, to also run the Python tests:
35
+ `pip install -e bindings/python[test]`
36
+
37
+ crates.io (used by .github/workflows/crates.yml):
38
+ - As an owner of the mpatch crate, open its Settings > Trusted Publishing
39
+ page on crates.io and add a GitHub publisher: owner romelium, repository
40
+ mpatch, workflow crates.yml, environment crates-io.
41
+ - Trusted Publishing can only be configured for a crate that exists on
42
+ crates.io. If the crate has never been published, publish the first
43
+ version by hand with an API token: `cargo login`, then
44
+ `cargo publish -p mpatch`.
45
+
46
+ PyPI (used by .github/workflows/pypi.yml):
47
+ - As an owner of the mpatch project, open its Manage > Publishing page on
48
+ PyPI and add a GitHub publisher: owner romelium, repository mpatch,
49
+ workflow pypi.yml, environment pypi.
50
+ - If the project does not exist on PyPI yet, add the same details as a
51
+ pending publisher from your account's Publishing page instead; the first
52
+ upload then creates the project.
53
+
54
+ GitHub (optional):
55
+ - Signing: release.yml signs the release archives and attaches public.key
56
+ only when the repository secrets GPG_PRIVATE_KEY and GPG_PASSPHRASE are
57
+ set. Create a key with `gpg --full-generate-key`, then under Settings >
58
+ Secrets and variables > Actions store the output of
59
+ `gpg --armor --export-secret-keys <key-id>` as GPG_PRIVATE_KEY and the
60
+ key's passphrase as GPG_PASSPHRASE.
61
+ - Approval: the publish jobs run in the crates-io and pypi environments.
62
+ To approve each upload before it runs, add required reviewers to those
63
+ environments under Settings > Environments.
64
+
65
+ Each release
66
+ ------------
67
+ Before running the script:
68
+ 1. Merge everything into main, run `git checkout main && git pull`, and
69
+ make sure CI passes. The working tree must be clean.
70
+ 2. Write the release notes under `## [Unreleased]` in CHANGELOG.md. The
71
+ script renames that section to `## [X.Y.Z] - <date>`, and release.yml
72
+ uses it as the GitHub release notes.
73
+ 3. Do a dry run, e.g. `python scripts/release.py minor --dry-run`.
74
+
75
+ What the script does:
76
+ 1. Checks the tools, the git identity, that you are on main with a clean
77
+ tree that is not behind the remote, and that the tag does not exist yet.
78
+ 2. Runs cargo fmt --check, cargo clippy (-D warnings), cargo doc
79
+ (-D warnings), cargo package, cargo test, ruff on bindings/python and
80
+ scripts/, and pytest when the bindings are installed.
81
+ 3. Bumps the version in Cargo.toml, bindings/python/Cargo.toml,
82
+ bindings/python/pyproject.toml, CHANGELOG.md, src/lib.rs and README.md,
83
+ and syncs Cargo.lock.
84
+ 4. Commits "chore: release vX.Y.Z", creates the annotated tag vX.Y.Z, and
85
+ pushes the branch and the tag.
86
+ If a step fails before the commit, the changed files are restored.
87
+
88
+ After the script:
89
+ 1. The pushed tag runs .github/workflows/release.yml, which builds the
90
+ binaries for every target and creates a *draft* GitHub release.
91
+ 2. When that finishes, open the draft under Releases on GitHub, check its
92
+ assets and notes, and click "Publish release".
93
+ 3. Publishing runs .github/workflows/crates.yml (crates.io) and
94
+ .github/workflows/pypi.yml (PyPI) in parallel.
95
+ 4. Check the new version on crates.io, docs.rs and PyPI.
96
+ If a publish workflow fails, re-run its failed jobs from the Actions tab.
97
+ To start one manually instead, use "Run workflow" and pick the release tag
98
+ as the ref; running it on main publishes whatever version main contains.
99
+ """
2
100
 
3
101
  from __future__ import annotations
4
102
 
@@ -248,25 +346,8 @@ def check_python_module(module_name: str) -> bool:
248
346
  return False
249
347
 
250
348
 
251
- def verify_crates_io_auth() -> bool:
252
- """Checks if credentials exist for publishing to crates.io."""
253
- if os.environ.get("CARGO_REGISTRY_TOKEN") or os.environ.get(
254
- "CARGO_REGISTRIES_CRATES_IO_TOKEN"
255
- ):
256
- return True
257
- home = Path.home()
258
- cred_toml = home / ".cargo" / "credentials.toml"
259
- cred_plain = home / ".cargo" / "credentials"
260
- for p in [cred_toml, cred_plain]:
261
- if p.exists() and "token" in p.read_text(encoding="utf-8", errors="ignore"):
262
- return True
263
- return False
264
-
265
-
266
349
  # --- Pre-flight Tool Diagnostics ---
267
- def run_tool_diagnostics(
268
- root: Path, skip_tests: bool, publish_cargo: bool
269
- ) -> list[str]:
350
+ def run_tool_diagnostics(root: Path, skip_tests: bool) -> list[str]:
270
351
  """Validates all required tools and runtime environments before execution."""
271
352
  info("Step 1: Running Pre-flight Environment & Tool Diagnostics")
272
353
  issues: list[str] = []
@@ -375,13 +456,6 @@ def run_tool_diagnostics(
375
456
  else:
376
457
  print(" - pytest: not installed in active Python environment (optional)")
377
458
 
378
- # 6. Crates.io authentication check (if cargo publish requested)
379
- if publish_cargo and not verify_crates_io_auth():
380
- warn(
381
- "No crates.io token detected in CARGO_REGISTRY_TOKEN or ~/.cargo/credentials[.toml]. "
382
- "'cargo publish' may prompt or fail if unauthenticated."
383
- )
384
-
385
459
  return issues
386
460
 
387
461
 
@@ -428,7 +502,11 @@ def apply_version_bumps(ctx: ReleaseContext, current_ver: str, new_ver: str) ->
428
502
  """Applies version updates across Cargo.toml files, pyproject.toml, CHANGELOG, docs, and lockfile."""
429
503
  root = ctx.root
430
504
  dry_run = ctx.dry_run
431
- today = datetime.datetime.now(datetime.timezone.utc).date().isoformat() # noqa: UP017
505
+ today = (
506
+ datetime.datetime.now(datetime.timezone(datetime.timedelta(0)))
507
+ .date()
508
+ .isoformat()
509
+ )
432
510
  c_esc = re.escape(current_ver)
433
511
 
434
512
  # 1. Root Cargo.toml
@@ -676,13 +754,6 @@ def main() -> None:
676
754
  help="Do not push git commit and tag to remote.",
677
755
  )
678
756
 
679
- parser.add_argument(
680
- "--publish-cargo",
681
- action="store_true",
682
- default=False,
683
- help="Publish the core crate to crates.io after tagging.",
684
- )
685
-
686
757
  args = parser.parse_args()
687
758
  root = get_repo_root()
688
759
  os.chdir(root)
@@ -700,7 +771,7 @@ def main() -> None:
700
771
 
701
772
  try:
702
773
  # --- Step 1: Pre-flight Tool Diagnostics ---
703
- issues = run_tool_diagnostics(root, args.skip_tests, args.publish_cargo)
774
+ issues = run_tool_diagnostics(root, args.skip_tests)
704
775
  if issues:
705
776
  error("Pre-flight checks identified blocking issues:")
706
777
  for issue in issues:
@@ -919,38 +990,7 @@ def main() -> None:
919
990
  f"&& git push {remote_name} {tag_name}"
920
991
  )
921
992
 
922
- # --- Step 7: Crates.io Publishing ---
923
- print()
924
- info("Step 7: Publishing to Crates.io")
925
- pkg_name = read_package_name(root)
926
- should_publish_cargo = args.publish_cargo
927
- if not should_publish_cargo and not args.dry_run and not args.yes:
928
- ans = (
929
- input("Do you want to publish the Rust crate to Crates.io now? [y/N]: ")
930
- .strip()
931
- .lower()
932
- )
933
- should_publish_cargo = ans == "y"
934
-
935
- if should_publish_cargo:
936
- if args.dry_run:
937
- info(f"[DRY RUN] Would execute: cargo publish -p {pkg_name}")
938
- else:
939
- try:
940
- run_cmd(["cargo", "publish", "-p", pkg_name], cwd=root)
941
- success(f"Published {pkg_name} to Crates.io!")
942
- except (RuntimeError, OSError) as e:
943
- warn(
944
- f"Crates.io publish failed ({e}). "
945
- f"You can publish manually later with: cargo publish -p {pkg_name}"
946
- )
947
- else:
948
- info(
949
- f"Skipped cargo publish. Publish manually later using: "
950
- f"cargo publish -p {pkg_name}"
951
- )
952
-
953
- # --- Step 8: Summary & Guidance ---
993
+ # --- Step 7: Summary & Guidance ---
954
994
  print(f"\n{Style.BOLD}{'=' * 56}{Style.RESET}")
955
995
  print(
956
996
  f"{Style.GREEN}{Style.BOLD} "
@@ -966,14 +1006,18 @@ def main() -> None:
966
1006
  f" It compiles multi-platform binaries and creates a "
967
1007
  f"{Style.BOLD}Draft Release{Style.RESET}."
968
1008
  )
969
- print(f" 2. {Style.BOLD}Publish Draft on GitHub & Trigger PyPI:{Style.RESET}")
1009
+ print(
1010
+ f" 2. {Style.BOLD}Publish Draft on GitHub & Trigger crates.io + PyPI:"
1011
+ f"{Style.RESET}"
1012
+ )
970
1013
  print(" • Navigate to your repository -> Releases.")
971
1014
  print(
972
1015
  f" • Open Draft Release {tag_name} and click "
973
1016
  f"{Style.GREEN}'Publish Release'{Style.RESET}."
974
1017
  )
975
- print(" • This automatically triggers `.github/workflows/pypi.yml` to build")
976
- print(" and publish all Python wheels to PyPI.")
1018
+ print(" • This automatically triggers `.github/workflows/crates.yml` to")
1019
+ print(" publish the crate to crates.io and `.github/workflows/pypi.yml`")
1020
+ print(" to build and publish all Python wheels to PyPI.")
977
1021
  print()
978
1022
 
979
1023
  except KeyboardInterrupt: