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.
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.github/workflows/ci.yml +1 -0
- outlook_cli_rs-0.2.3/.github/workflows/release.yml +179 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/CHANGELOG.md +11 -2
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Cargo.lock +1 -1
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Cargo.toml +1 -1
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/PKG-INFO +17 -5
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/README.md +14 -1
- outlook_cli_rs-0.2.3/docs/releases.md +70 -0
- outlook_cli_rs-0.2.3/scripts/release.py +167 -0
- outlook_cli_rs-0.2.3/vership.toml +11 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.github/dependabot.yml +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/.gitignore +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/LICENSE +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/Makefile +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/pyproject.toml +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/auth.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/backend.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/cli.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/config.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/desktop.ps1 +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/desktop.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/error.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/graph.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/lib.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/main.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/output.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/presentation.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/src/schema.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/cli.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/desktop.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/desktop_bridge.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/fixtures/clispec-v0.3.json +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/fixtures/desktop-mock.ps1 +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/graph.rs +0 -0
- {outlook_cli_rs-0.2.2 → outlook_cli_rs-0.2.3}/tests/presentation.rs +0 -0
|
@@ -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.
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: outlook-cli-rs
|
|
3
|
-
Version: 0.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,
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|