outlook-cli-rs 0.2.2__tar.gz → 0.2.3__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 (35) hide show
  1. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.github/workflows/ci.yml +1 -0
  2. outlook_cli_rs-0.2.3/.github/workflows/release.yml +179 -0
  3. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/CHANGELOG.md +11 -2
  4. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Cargo.lock +1 -1
  5. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Cargo.toml +1 -1
  6. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/PKG-INFO +17 -5
  7. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/README.md +14 -1
  8. outlook_cli_rs-0.2.3/docs/releases.md +70 -0
  9. outlook_cli_rs-0.2.3/scripts/release.py +167 -0
  10. outlook_cli_rs-0.2.3/vership.toml +11 -0
  11. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.github/dependabot.yml +0 -0
  12. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.gitignore +0 -0
  13. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/LICENSE +0 -0
  14. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Makefile +0 -0
  15. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/pyproject.toml +0 -0
  16. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/auth.rs +0 -0
  17. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/backend.rs +0 -0
  18. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/cli.rs +0 -0
  19. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/config.rs +0 -0
  20. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/desktop.ps1 +0 -0
  21. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/desktop.rs +0 -0
  22. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/error.rs +0 -0
  23. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/graph.rs +0 -0
  24. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/lib.rs +0 -0
  25. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/main.rs +0 -0
  26. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/output.rs +0 -0
  27. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/presentation.rs +0 -0
  28. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/schema.rs +0 -0
  29. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/cli.rs +0 -0
  30. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/desktop.rs +0 -0
  31. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/desktop_bridge.rs +0 -0
  32. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/fixtures/clispec-v0.3.json +0 -0
  33. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/fixtures/desktop-mock.ps1 +0 -0
  34. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/graph.rs +0 -0
  35. {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/presentation.rs +0 -0
@@ -3,6 +3,7 @@ name: CI
3
3
  on:
4
4
  push:
5
5
  pull_request:
6
+ workflow_call:
6
7
 
7
8
  permissions:
8
9
  contents: read
@@ -0,0 +1,179 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+ workflow_dispatch:
7
+ inputs:
8
+ publish:
9
+ description: Publish the verified packages (requires a version tag and registry secrets)
10
+ type: boolean
11
+ default: false
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: release-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ test:
22
+ uses: ./.github/workflows/ci.yml
23
+
24
+ wheels:
25
+ name: Wheel (${{ matrix.target }})
26
+ needs: test
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ include:
31
+ - os: ubuntu-24.04
32
+ target: x86_64-unknown-linux-gnu
33
+ compatibility: '2014'
34
+ container: quay.io/pypa/manylinux2014_x86_64:latest
35
+ - os: ubuntu-24.04-arm
36
+ target: aarch64-unknown-linux-gnu
37
+ compatibility: '2014'
38
+ container: quay.io/pypa/manylinux2014_aarch64:latest
39
+ - os: ubuntu-24.04
40
+ target: x86_64-unknown-linux-musl
41
+ compatibility: musllinux_1_2
42
+ container: quay.io/pypa/musllinux_1_2_x86_64:latest
43
+ - os: ubuntu-24.04-arm
44
+ target: aarch64-unknown-linux-musl
45
+ compatibility: musllinux_1_2
46
+ container: quay.io/pypa/musllinux_1_2_aarch64:latest
47
+ - os: windows-latest
48
+ target: x86_64-pc-windows-msvc
49
+ compatibility: auto
50
+ container: off
51
+ - os: macos-15-intel
52
+ target: x86_64-apple-darwin
53
+ compatibility: auto
54
+ container: off
55
+ - os: macos-latest
56
+ target: aarch64-apple-darwin
57
+ compatibility: auto
58
+ container: off
59
+ runs-on: ${{ matrix.os }}
60
+ steps:
61
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
62
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
63
+ with:
64
+ python-version: '3.12'
65
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
66
+ - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
67
+ with:
68
+ command: build
69
+ args: --release --locked --out dist
70
+ target: ${{ matrix.target }}
71
+ manylinux: ${{ matrix.compatibility }}
72
+ container: ${{ matrix.container }}
73
+ rust-toolchain: '1.90.0'
74
+ before-script-linux: |
75
+ if command -v apk >/dev/null 2>&1; then
76
+ apk add --no-cache cmake make perl pkgconf
77
+ # Vendored D-Bus otherwise calls GCC atomic helpers that Rust's
78
+ # static ARM64 musl link does not provide. Use inline atomics.
79
+ if [ "$(uname -m)" = aarch64 ]; then
80
+ export CFLAGS_aarch64_unknown_linux_musl="-mno-outline-atomics"
81
+ fi
82
+ fi
83
+ - name: Verify uv installation without source builds
84
+ if: ${{ !contains(matrix.target, '-musl') }}
85
+ run: python scripts/release.py smoke dist
86
+ - name: Verify musl wheel in Alpine without Rust
87
+ if: contains(matrix.target, '-musl')
88
+ shell: bash
89
+ run: |
90
+ docker run --rm -v "$PWD:/io:ro" -w /io \
91
+ ghcr.io/astral-sh/uv:python3.12-alpine \
92
+ python scripts/release.py smoke dist
93
+ - name: Package standalone executable and checksum
94
+ run: python scripts/release.py archive dist --target ${{ matrix.target }}
95
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
96
+ with:
97
+ name: wheel-${{ matrix.target }}
98
+ path: dist/*
99
+ if-no-files-found: error
100
+
101
+ source:
102
+ needs: test
103
+ runs-on: ubuntu-latest
104
+ steps:
105
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
106
+ - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
107
+ with:
108
+ command: sdist
109
+ args: --out dist
110
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
111
+ with:
112
+ name: source
113
+ path: dist/*.tar.gz
114
+ if-no-files-found: error
115
+
116
+ verify:
117
+ needs: [wheels, source]
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
121
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
122
+ with:
123
+ python-version: '3.12'
124
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
125
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
126
+ with:
127
+ path: dist
128
+ merge-multiple: true
129
+ - run: python scripts/release.py verify dist
130
+ - run: uv tool run --from twine twine check dist/*.whl dist/outlook_cli_rs-*.tar.gz
131
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
132
+ with:
133
+ name: release-packages
134
+ path: dist/*
135
+ if-no-files-found: error
136
+
137
+ # Building a tag never publishes a partial platform set. Publication is an
138
+ # explicit dispatch on that tag after all build and installation checks pass.
139
+ publish:
140
+ if: github.event_name == 'workflow_dispatch' && inputs.publish
141
+ needs: verify
142
+ runs-on: ubuntu-latest
143
+ permissions:
144
+ contents: write
145
+ steps:
146
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
147
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
148
+ with:
149
+ python-version: '3.12'
150
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
151
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
152
+ with:
153
+ name: release-packages
154
+ path: dist
155
+ - name: Check tag and credentials before publishing anything
156
+ env:
157
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
158
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
159
+ run: |
160
+ python scripts/release.py verify dist --require-tag "$GITHUB_REF_NAME"
161
+ test "$GITHUB_REF_TYPE" = tag
162
+ test -n "$CARGO_REGISTRY_TOKEN"
163
+ test -n "$TWINE_PASSWORD"
164
+ - name: Publish crates.io package
165
+ env:
166
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
167
+ run: cargo publish --locked
168
+ - name: Publish all Python packages
169
+ env:
170
+ TWINE_USERNAME: __token__
171
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
172
+ run: uv tool run --from twine twine upload --non-interactive dist/*.whl dist/outlook_cli_rs-*.tar.gz
173
+ - name: Publish executable archives
174
+ env:
175
+ GH_TOKEN: ${{ github.token }}
176
+ run: |
177
+ python scripts/release.py notes > release-notes.md
178
+ gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" \
179
+ --notes-file release-notes.md dist/outlook-v* dist/SHA256SUMS
@@ -1,6 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.2 - 2026-09-08
3
+ ## [0.2.3] - 2026-09-08
4
+
5
+ ### Fixed
6
+
7
+ - Ship prebuilt Python wheels for Windows x64, Intel and Apple Silicon macOS, and x64/ARM64 Linux (glibc and musl), so supported `uv` installations do not need Rust.
8
+ - Build and verify the full platform matrix before publishing, including `uv` installation with source builds disabled and standalone executable smoke tests.
9
+ - Provide executable archives and SHA-256 checksums for every wheel target.
10
+ - Clarify that source installations on other platforms require Rust and native build tools.
11
+
12
+ ## [0.2.2] - 2026-09-08
4
13
 
5
14
  ### Added
6
15
 
@@ -18,7 +27,7 @@
18
27
  - Attachments and calendar commands remain Graph-only.
19
28
  - Desktop bridge tests pass with mock Outlook objects; live Windows/WSL COM validation remains outstanding.
20
29
 
21
- ## 0.2.1 - 2026-09-08
30
+ ## [0.2.1] - 2026-09-08
22
31
 
23
32
  ### Added
24
33
 
@@ -1365,7 +1365,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
1365
1365
 
1366
1366
  [[package]]
1367
1367
  name = "outlook-cli"
1368
- version = "0.2.2"
1368
+ version = "0.2.3"
1369
1369
  dependencies = [
1370
1370
  "assert_cmd",
1371
1371
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "outlook-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  edition = "2024"
5
5
  rust-version = "1.90"
6
6
  description = "Microsoft Outlook from your terminal, for humans and agents"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: outlook-cli-rs
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -13,14 +13,13 @@ Classifier: Topic :: Communications :: Email
13
13
  Classifier: Topic :: Office/Business :: Scheduling
14
14
  License-File: LICENSE
15
15
  Summary: Microsoft Outlook from your terminal, for humans and agents
16
- Keywords: outlook,microsoft-graph,email,calendar,cli
17
16
  Home-Page: https://github.com/rvben/outlook-cli
18
- License: MIT
17
+ License-Expression: MIT
19
18
  Requires-Python: >=3.8
20
19
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
20
  Project-URL: Homepage, https://github.com/rvben/outlook-cli
22
- Project-URL: Repository, https://github.com/rvben/outlook-cli
23
21
  Project-URL: Issues, https://github.com/rvben/outlook-cli/issues
22
+ Project-URL: Repository, https://github.com/rvben/outlook-cli
24
23
 
25
24
  # outlook-cli
26
25
 
@@ -60,12 +59,25 @@ OUTLOOK_TEST_POWERSHELL=/path/to/pwsh cargo test --test desktop_bridge -- --igno
60
59
 
61
60
  ```console
62
61
  cargo install outlook-cli --locked
63
- # or, without a Rust toolchain
62
+ # or, using a prebuilt wheel (supported platforms below)
64
63
  uv tool install outlook-cli-rs
65
64
  ```
66
65
 
67
66
  Both packages install the `outlook` executable.
68
67
 
68
+ Starting with 0.2.3, PyPI wheels cover Windows x64, macOS Intel/Apple Silicon,
69
+ and Linux x64/ARM64 (glibc 2.17+ or musl 1.2+). WSL uses the Linux wheel.
70
+ These wheels contain the compiled executable, so Rust is not needed to install
71
+ or run them. Other platforms require Rust and native build tools to build the
72
+ source distribution. To require a prebuilt package and fail instead of compiling:
73
+
74
+ ```sh
75
+ uv tool install --no-build outlook-cli-rs
76
+ ```
77
+
78
+ See [the release process](docs/releases.md) for the build matrix and publication checks.
79
+
80
+
69
81
  ## Configure
70
82
 
71
83
  `outlook-cli` includes a maintained multitenant Microsoft Entra public-client
@@ -36,12 +36,25 @@ OUTLOOK_TEST_POWERSHELL=/path/to/pwsh cargo test --test desktop_bridge -- --igno
36
36
 
37
37
  ```console
38
38
  cargo install outlook-cli --locked
39
- # or, without a Rust toolchain
39
+ # or, using a prebuilt wheel (supported platforms below)
40
40
  uv tool install outlook-cli-rs
41
41
  ```
42
42
 
43
43
  Both packages install the `outlook` executable.
44
44
 
45
+ Starting with 0.2.3, PyPI wheels cover Windows x64, macOS Intel/Apple Silicon,
46
+ and Linux x64/ARM64 (glibc 2.17+ or musl 1.2+). WSL uses the Linux wheel.
47
+ These wheels contain the compiled executable, so Rust is not needed to install
48
+ or run them. Other platforms require Rust and native build tools to build the
49
+ source distribution. To require a prebuilt package and fail instead of compiling:
50
+
51
+ ```sh
52
+ uv tool install --no-build outlook-cli-rs
53
+ ```
54
+
55
+ See [the release process](docs/releases.md) for the build matrix and publication checks.
56
+
57
+
45
58
  ## Configure
46
59
 
47
60
  `outlook-cli` includes a maintained multitenant Microsoft Entra public-client
@@ -0,0 +1,70 @@
1
+ # Releases
2
+
3
+ The release workflow follows rumdl's seven-target wheel matrix:
4
+
5
+ | Platform | Architecture | Wheel baseline |
6
+ | --- | --- | --- |
7
+ | Windows | x64 | MSVC |
8
+ | macOS | Intel x64 / Apple Silicon ARM64 | Maturin's deployment target |
9
+ | Linux / WSL | x64 / ARM64 | manylinux2014 (glibc 2.17+) |
10
+ | Alpine Linux | x64 / ARM64 | musllinux 1.2 |
11
+
12
+ Linux wheels build in the corresponding PyPA containers on native architecture
13
+ runners. Every wheel is installed using `uv tool install --no-build --no-index`
14
+ and its CLI version and schema are checked. Musl wheels run in Alpine. The
15
+ standalone executable is also tested outside the wheel installation tree.
16
+
17
+ All seven wheels, the source package, all executable archives, and their checksums
18
+ must pass validation before the `release-packages` artifact is produced. A partial
19
+ matrix cannot enter the publish job. Existing CI tests, including the Windows
20
+ PowerShell mock bridge tests, run before packaging.
21
+
22
+ ## Prepare and build
23
+
24
+ Vership owns versioning, changelog generation, release commits, tags, and pushes.
25
+ From a clean main branch, prepare the next patch with:
26
+
27
+ ```sh
28
+ vership bump patch --prepare
29
+ ```
30
+
31
+ For a version already set in the manifests, use `vership release --prepare`
32
+ instead. Review the staged paths before committing any local working material;
33
+ release commits must exclude design reports and screenshots.
34
+
35
+ After reviewing the release commit, `vership release` runs its checks and pushes
36
+ the branch and version tag. The tag starts the seven-platform build. Tags do not
37
+ automatically publish to registries.
38
+
39
+ ```sh
40
+ vership release
41
+ gh run watch RUN_ID --exit-status
42
+ gh run download RUN_ID --name release-packages --dir dist/VERSION
43
+ python scripts/release.py verify dist/VERSION --require-tag vVERSION
44
+ ```
45
+
46
+ A build-only preview is also available with
47
+ `gh workflow run release.yml --ref main -f publish=false`.
48
+
49
+ ## Publish
50
+
51
+ For automated publication, configure repository secrets `CARGO_REGISTRY_TOKEN`
52
+ and `PYPI_API_TOKEN`, then explicitly dispatch `Release` on the version tag with
53
+ `publish=true`. The workflow rebuilds and verifies the matrix, checks the tag and
54
+ both credentials, publishes crates.io and PyPI packages, and creates a GitHub
55
+ release with standalone archives. Credentials are not needed for build-only runs.
56
+
57
+ When publishing locally with existing credentials, use the verified artifact from
58
+ the **same release commit**, without rebuilding just the host wheel:
59
+
60
+ ```sh
61
+ cargo publish --locked
62
+ uv tool run --from twine twine check dist/VERSION/*.whl dist/VERSION/outlook_cli_rs-*.tar.gz
63
+ uv tool run --from twine twine upload --non-interactive dist/VERSION/*.whl dist/VERSION/outlook_cli_rs-*.tar.gz
64
+ ```
65
+
66
+ Run `vership verify VERSION` after publication to check the tag, GitHub release,
67
+ crates.io, and PyPI. Also verify published asset SHA-256 digests. Never replace a
68
+ published version or move its tag. If anything was published, prepare a new patch
69
+ version for fixes; do not rerun the publish job against an already published
70
+ version. If nothing was published, repair the build and retry the same version.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """Package and verify the complete, installable release matrix using stdlib only."""
3
+ import argparse
4
+ import hashlib
5
+ import io
6
+ import json
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+ import subprocess
11
+ import sys
12
+ import tarfile
13
+ import tempfile
14
+ import tomllib
15
+ import zipfile
16
+
17
+ ROOT = Path(__file__).resolve().parent.parent
18
+ VERSION = tomllib.loads((ROOT / 'Cargo.toml').read_text())['package']['version']
19
+ TARGETS = {
20
+ 'x86_64-unknown-linux-gnu': ('manylinux', 'x86_64'),
21
+ 'aarch64-unknown-linux-gnu': ('manylinux', 'aarch64'),
22
+ 'x86_64-unknown-linux-musl': ('musllinux_1_2', 'x86_64'),
23
+ 'aarch64-unknown-linux-musl': ('musllinux_1_2', 'aarch64'),
24
+ 'x86_64-pc-windows-msvc': ('win_amd64', ''),
25
+ 'x86_64-apple-darwin': ('macosx', 'x86_64'),
26
+ 'aarch64-apple-darwin': ('macosx', 'arm64'),
27
+ }
28
+
29
+
30
+ def wheels(directory):
31
+ return sorted(directory.glob(f'outlook_cli_rs-{VERSION}-*.whl'))
32
+
33
+
34
+ def wheel_target(path):
35
+ platform = path.stem.split('-')[-1]
36
+ matches = [target for target, (prefix, arch) in TARGETS.items()
37
+ if platform.startswith(prefix) and platform.endswith(arch)]
38
+ if len(matches) != 1:
39
+ raise ValueError(f'Unexpected wheel platform: {path.name}')
40
+ return matches[0]
41
+
42
+
43
+ def executable(wheel):
44
+ with zipfile.ZipFile(wheel) as archive:
45
+ entries = [n for n in archive.namelist()
46
+ if n.endswith(('.data/scripts/outlook', '.data/scripts/outlook.exe'))]
47
+ if len(entries) != 1:
48
+ raise ValueError(f'Expected one outlook executable in {wheel}')
49
+ return Path(entries[0]).name, archive.read(entries[0])
50
+
51
+
52
+ def smoke(directory):
53
+ candidates = wheels(directory)
54
+ if len(candidates) != 1:
55
+ raise ValueError('Smoke test requires exactly one wheel')
56
+ with tempfile.TemporaryDirectory(prefix='outlook-wheel-') as temporary:
57
+ root = Path(temporary)
58
+ env = dict(os.environ, UV_TOOL_DIR=str(root / 'tools'),
59
+ UV_TOOL_BIN_DIR=str(root / 'bin'), UV_CACHE_DIR=str(root / 'cache'),
60
+ UV_PYTHON_DOWNLOADS='never')
61
+ subprocess.run(['uv', 'tool', 'install', '--no-build', '--no-index',
62
+ '--python', sys.executable, '--find-links', str(directory.resolve()),
63
+ f'outlook-cli-rs=={VERSION}'], env=env, check=True)
64
+ binary = root / 'bin' / ('outlook.exe' if os.name == 'nt' else 'outlook')
65
+ assert subprocess.check_output([binary, '--version'], text=True).strip() == f'outlook {VERSION}'
66
+ schema = json.loads(subprocess.check_output([binary, 'schema'], text=True))
67
+ assert 'mail draft send' in json.dumps(schema)
68
+ # Also execute the standalone bytes from the wheel, without its install
69
+ # tree. This catches accidental runtime dependence on bundled libraries.
70
+ name, data = executable(candidates[0])
71
+ standalone = root / name
72
+ standalone.write_bytes(data)
73
+ standalone.chmod(0o755)
74
+ assert subprocess.check_output([standalone, '--version'], text=True).strip() == f'outlook {VERSION}'
75
+ print(f'Installed and executed {candidates[0].name} with source builds disabled')
76
+
77
+
78
+ def package(directory, target):
79
+ candidates = wheels(directory)
80
+ if len(candidates) != 1 or wheel_target(candidates[0]) != target:
81
+ raise ValueError('Expected one wheel matching the requested target')
82
+ name, data = executable(candidates[0])
83
+ stem = f'outlook-v{VERSION}-{target}'
84
+ files = {name: data, 'LICENSE': (ROOT / 'LICENSE').read_bytes(),
85
+ 'README.md': (ROOT / 'README.md').read_bytes()}
86
+ if target.endswith('windows-msvc'):
87
+ path = directory / f'{stem}.zip'
88
+ with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
89
+ for filename, content in files.items():
90
+ archive.writestr(filename, content)
91
+ else:
92
+ path = directory / f'{stem}.tar.gz'
93
+ with tarfile.open(path, 'w:gz') as archive:
94
+ for filename, content in files.items():
95
+ info = tarfile.TarInfo(filename)
96
+ info.size = len(content)
97
+ info.mode = 0o755 if filename == name else 0o644
98
+ archive.addfile(info, io.BytesIO(content))
99
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
100
+ path.with_name(path.name + '.sha256').write_text(f'{digest} {path.name}\n')
101
+ print(f'Packaged {path.name}')
102
+
103
+
104
+ def verify(directory, tag=None):
105
+ if tag is not None and tag != f'v{VERSION}':
106
+ raise ValueError(f'Tag must match Cargo.toml: v{VERSION}')
107
+ candidates = wheels(directory)
108
+ actual = [wheel_target(path) for path in candidates]
109
+ if len(actual) != len(TARGETS) or set(actual) != set(TARGETS):
110
+ raise ValueError(f'Incomplete or duplicate wheel matrix: {actual}')
111
+ expected = set(candidates)
112
+ for target in TARGETS:
113
+ suffix = '.zip' if target.endswith('windows-msvc') else '.tar.gz'
114
+ path = directory / f'outlook-v{VERSION}-{target}{suffix}'
115
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
116
+ checksum = path.with_name(path.name + '.sha256')
117
+ if checksum.read_text().strip() != f'{digest} {path.name}':
118
+ raise ValueError(f'Checksum mismatch: {path.name}')
119
+ expected.update((path, checksum))
120
+ source = directory / f'outlook_cli_rs-{VERSION}.tar.gz'
121
+ with tarfile.open(source) as archive:
122
+ manifest = archive.extractfile(f'outlook_cli_rs-{VERSION}/Cargo.toml')
123
+ if manifest is None or tomllib.loads(manifest.read().decode())['package']['version'] != VERSION:
124
+ raise ValueError('Source package version mismatch')
125
+ expected.add(source)
126
+ unexpected = set(directory.iterdir()) - expected - {directory / 'SHA256SUMS'}
127
+ if unexpected:
128
+ raise ValueError(f'Unexpected release files: {unexpected}')
129
+ lines = [f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p.name}\n'
130
+ for p in sorted(expected) if not p.name.endswith('.sha256')]
131
+ (directory / 'SHA256SUMS').write_text(''.join(lines))
132
+ print(f'Verified {len(candidates)} wheels, source package, and {len(TARGETS)} archives')
133
+
134
+
135
+ def notes():
136
+ text = (ROOT / 'CHANGELOG.md').read_text()
137
+ heading = re.search(rf'^## \[{re.escape(VERSION)}\](?:\([^\n]*\))?[^\n]*$', text, re.M)
138
+ if heading is None:
139
+ raise ValueError(f'No changelog section for {VERSION}')
140
+ end = text.find('\n## ', heading.end())
141
+ print(text[heading.start():end if end != -1 else None].strip())
142
+
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(description=__doc__)
146
+ sub = parser.add_subparsers(dest='command', required=True)
147
+ for name in ('smoke', 'archive', 'verify'):
148
+ command = sub.add_parser(name)
149
+ command.add_argument('directory', type=Path)
150
+ if name == 'archive':
151
+ command.add_argument('--target', choices=TARGETS, required=True)
152
+ if name == 'verify':
153
+ command.add_argument('--require-tag')
154
+ sub.add_parser('notes')
155
+ args = parser.parse_args()
156
+ if args.command == 'smoke':
157
+ smoke(args.directory)
158
+ elif args.command == 'archive':
159
+ package(args.directory, args.target)
160
+ elif args.command == 'verify':
161
+ verify(args.directory, args.require_tag)
162
+ else:
163
+ notes()
164
+
165
+
166
+ if __name__ == '__main__':
167
+ main()
@@ -0,0 +1,11 @@
1
+ [project]
2
+ branch = "main"
3
+
4
+ [changelog]
5
+ unconventional = "exclude"
6
+
7
+ [checks]
8
+ lint = true
9
+ tests = true
10
+ lint_command = "cargo fmt -- --check && cargo clippy --locked --all-targets -- -D warnings"
11
+ test_command = "cargo test --locked --all-targets"
File without changes
File without changes