gitoxide-py 0.87.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.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Install the cross-built Windows wheel into a real Windows CPython and import
3
+ # it. Runs inside infra's windows-ci image (Wine plus a Windows Python), which
4
+ # the release workflow uses as a job container with the wheels mounted beside
5
+ # this script.
6
+ #
7
+ # This is the check the GitHub-hosted windows runner never performed: it built
8
+ # a wheel and uploaded it without ever loading the extension. A wheel that
9
+ # links a DLL the target machine lacks fails here rather than on a user's
10
+ # machine after publishing.
11
+ set -euo pipefail
12
+
13
+ # The directory this script was staged into is the one holding the wheels.
14
+ cd "$(dirname "$0")"
15
+
16
+ # --only-binary: the sdist would also satisfy `gitoxide-py`, and building it
17
+ # would need a Rust toolchain this image does not have -- a silent fallback
18
+ # would mean testing something other than the wheel under test.
19
+ # --no-index: everything needed is in that directory, and the container may
20
+ # have no route to PyPI.
21
+ xvfb-run --auto-servernum wine C:/Python312/python.exe -m pip install \
22
+ --no-index --only-binary :all: --find-links . gitoxide-py
23
+
24
+ xvfb-run --auto-servernum wine C:/Python312/python.exe - <<'PY'
25
+ import pathlib
26
+
27
+ import gitoxide_py
28
+
29
+ pkg = pathlib.Path(gitoxide_py.__file__).parent
30
+ assert (pkg / "py.typed").is_file(), "py.typed missing from the wheel"
31
+ assert (pkg / "__init__.pyi").is_file(), "stub missing from the wheel"
32
+
33
+ # Calls into the extension, so a wheel that imports but cannot execute fails
34
+ # here too.
35
+ print("windows wheel ok:", gitoxide_py.gix_version())
36
+ PY
@@ -0,0 +1,128 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ concurrency:
10
+ group: ci-${{ github.event.pull_request.number || github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ name: Test
19
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
20
+ runs-on: [self-hosted, feguitv]
21
+ timeout-minutes: 20
22
+ # git, uv, and a pinned Rust toolchain (with rustfmt/clippy) all ship in
23
+ # this image already (infra's roles/python_ci), so no per-run setup is
24
+ # needed for any of them.
25
+ container:
26
+ image: localhost:5000/python-ci:latest
27
+ defaults:
28
+ run:
29
+ shell: bash
30
+ steps:
31
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
32
+ with:
33
+ fetch-depth: 0 # full history, so `git describe` can see tags
34
+
35
+ - name: Configure git identity for test fixtures
36
+ run: |
37
+ git config --global user.name "CI"
38
+ git config --global user.email "ci@localhost"
39
+ git config --global --add safe.directory '*'
40
+
41
+ - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
42
+
43
+ # dunamai derives a version from git state: the exact tag on a tagged
44
+ # commit (a release build on main), otherwise the last tag plus a
45
+ # commit-count/short-SHA suffix so a dev build is traceable to the
46
+ # exact commit it came from. `uv tool run` (the `uvx` shorthand isn't
47
+ # installed in this image) runs it without adding it to the project's
48
+ # own dependencies. `--style semver` is required here (not pep440):
49
+ # Cargo.toml needs strict SemVer, and maturin itself converts a
50
+ # SemVer prerelease/build version into the equivalent PEP 440 wheel
51
+ # version at build time.
52
+ - name: Set package version from git
53
+ run: |
54
+ version="$(uv tool run dunamai from git --style semver)"
55
+ echo "Package version: $version"
56
+ sed -i "0,/^version = \".*\"/s//version = \"$version\"/" Cargo.toml
57
+
58
+ # --locked fails instead of silently updating uv.lock if it's out of
59
+ # sync with pyproject.toml.
60
+ - name: uv sync
61
+ run: uv sync --locked
62
+
63
+ - name: cargo fmt --check
64
+ run: cargo fmt --check
65
+
66
+ - name: cargo clippy
67
+ run: cargo clippy --all-targets -- -D warnings
68
+
69
+ - name: Build extension
70
+ run: uv run maturin develop --release --generate-stubs
71
+
72
+ # `--no-sync` for the same reason the coverage step below needs it: a
73
+ # plain `uv run pytest` re-syncs the project first and swaps the `.so`
74
+ # maturin just installed for whatever build its cache holds for this
75
+ # version, so pytest would import an extension that predates the step
76
+ # above. `--no-sync` runs pytest against the environment as it stands.
77
+ - name: pytest
78
+ run: uv run --no-sync pytest -v
79
+
80
+ # `cargo llvm-cov show-env` prints the RUSTC_WRAPPER/LLVM_PROFILE_FILE/etc
81
+ # that make a `cargo build` instrumented and its profile data land where
82
+ # `cargo llvm-cov report` expects it. The Rust code here is only ever
83
+ # exercised through the Python extension module (there's no `cargo
84
+ # test` harness), so rather than the usual `cargo llvm-cov test`
85
+ # one-liner, the env is exported once, maturin builds the instrumented
86
+ # `.so` under it, pytest drives that `.so` to produce the profile data
87
+ # (the LLVM profiling runtime flushes it when the Python process exits,
88
+ # same as it would for a native Rust test binary), and only then is the
89
+ # report generated from the accumulated data.
90
+ - name: Set up coverage environment
91
+ run: |
92
+ cargo llvm-cov show-env --sh | sed -E "s/^export //; s/'//g" >> "$GITHUB_ENV"
93
+ cargo llvm-cov clean --workspace
94
+
95
+ # A debug build, not the `test` job's `--release`: the release profile's
96
+ # `lto = "thin"` + `codegen-units = 1` (Cargo.toml) inlines and merges
97
+ # code across unit boundaries aggressively enough to make LLVM's
98
+ # per-line coverage counters unreliable.
99
+ - name: Build extension (instrumented)
100
+ run: uv run maturin develop
101
+
102
+ # `--no-sync`: a plain `uv run pytest` re-checks the project against
103
+ # uv's own build/dependency cache first and, finding the local
104
+ # gitoxide-py package "out of sync" with what it last built, silently
105
+ # rebuilds and reinstalls it -- outside the coverage env set up above,
106
+ # so it clobbers the instrumented `.so` maturin just installed with an
107
+ # uninstrumented one before pytest ever imports it. `--no-sync` runs
108
+ # pytest against the environment as it stands.
109
+ - name: pytest (coverage)
110
+ run: uv run --no-sync pytest -v
111
+
112
+ - name: Generate coverage report
113
+ run: |
114
+ echo "## Rust Coverage Summary" >> "$GITHUB_STEP_SUMMARY"
115
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
116
+ cargo llvm-cov report --summary-only >> "$GITHUB_STEP_SUMMARY"
117
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
118
+
119
+ # PyO3's `#[pymethods]` macro emits a zero-width coverage region per
120
+ # generated method trampoline, collapsed onto the attribute's own
121
+ # line, that never registers a hit regardless of whether the method
122
+ # actually ran (e.g. Repository.head_commit's shows 0 despite being
123
+ # called throughout the test suite) -- a coverage-instrumentation
124
+ # artifact of the macro expansion, not a gap in testing. That caps
125
+ # this file's realistic ceiling below literal 100%; 97 leaves
126
+ # headroom for that artifact while still catching real regressions.
127
+ - name: Enforce coverage threshold
128
+ run: cargo llvm-cov report --fail-under-lines 97
@@ -0,0 +1,273 @@
1
+ name: Release
2
+
3
+ # Distributions are built for every push to main so a broken build surfaces
4
+ # then, not at tag time. Publishing happens only for a tag, which is also
5
+ # where the version comes from.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ tags: ["v*"]
10
+ workflow_dispatch:
11
+ inputs:
12
+ testpypi:
13
+ description: >-
14
+ Upload to TestPyPI. Exercises the publish path -- the OIDC exchange
15
+ and the upload itself -- which a tag would otherwise be the first
16
+ thing to try.
17
+ type: boolean
18
+ default: false
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ # Every job below mounts the same host directory at /dist, so the wheels move
24
+ # between images without going through actions/upload-artifact -- a billed,
25
+ # org-wide quota a release has no reason to depend on. This works because all
26
+ # three jobs land on the same machine: `feguitv` is a single host, and its
27
+ # runner requires a job container, which is what lets the Windows check simply
28
+ # *be* the Wine image rather than start one by hand. The run id keeps
29
+ # concurrent releases out of each other's directory.
30
+
31
+ jobs:
32
+ # macOS has no self-hosted equivalent here and cannot be cross-compiled to
33
+ # from Linux without an Apple SDK, so it runs on GitHub-hosted runners:
34
+ # minutes, and artifact storage to carry the wheel back. Set the repository
35
+ # variable HOSTED_RUNNERS to `true` to include them; with it unset a release
36
+ # ships the Linux, Windows and source distributions, and macOS builds from
37
+ # the sdist, which needs a Rust toolchain on the installing machine.
38
+ wheels-hosted:
39
+ name: ${{ matrix.target }}
40
+ if: vars.HOSTED_RUNNERS == 'true'
41
+ runs-on: ${{ matrix.runner }}
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - { runner: ubuntu-latest, target: aarch64-unknown-linux-gnu, args: "--zig" }
47
+ - { runner: macos-latest, target: x86_64-apple-darwin, args: "" }
48
+ - { runner: macos-latest, target: aarch64-apple-darwin, args: "" }
49
+ steps:
50
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
51
+ with:
52
+ fetch-depth: 0
53
+
54
+ - uses: actions/setup-python@v6
55
+ with:
56
+ python-version: "3.13"
57
+
58
+ # `gix` is pure Rust with no C dependencies (see Cargo.toml), so a
59
+ # cross-compile needs no target toolchain beyond what --zig provides.
60
+ - name: Set package version from git
61
+ shell: bash
62
+ run: |
63
+ pipx install dunamai
64
+ version="$(dunamai from git --style semver)"
65
+ echo "Package version: $version"
66
+ if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
67
+ python scripts/check_version_tracks_gix.py "$version"
68
+ fi
69
+ python - "$version" <<'PY'
70
+ import re, sys
71
+ from pathlib import Path
72
+ path = Path("Cargo.toml")
73
+ text = path.read_text()
74
+ path.write_text(re.sub(r'^version = ".*"', f'version = "{sys.argv[1]}"', text, count=1, flags=re.M))
75
+ PY
76
+
77
+ - name: Build wheel
78
+ uses: PyO3/maturin-action@v1
79
+ with:
80
+ target: ${{ matrix.target }}
81
+ args: --release --out dist --generate-stubs ${{ matrix.args }}
82
+ manylinux: auto
83
+
84
+ - uses: actions/upload-artifact@v4
85
+ with:
86
+ name: wheels-${{ matrix.target }}
87
+ path: dist
88
+
89
+ # Everything buildable on Linux: the native wheel, the Windows wheel
90
+ # (cross-compiled with the mingw-w64 toolchain the image carries, see
91
+ # infra's roles/python_ci) and the sdist.
92
+ build:
93
+ name: Build
94
+ runs-on: [self-hosted, feguitv]
95
+ timeout-minutes: 40
96
+ container:
97
+ image: localhost:5000/python-ci:latest
98
+ volumes:
99
+ - /var/tmp/gitoxide-py-release/${{ github.run_id }}:/dist
100
+ defaults:
101
+ run:
102
+ shell: bash
103
+ steps:
104
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
105
+ with:
106
+ fetch-depth: 0 # full history, so the version can be derived from tags
107
+
108
+ - run: git config --global --add safe.directory '*'
109
+
110
+ - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
111
+
112
+ - name: Set package version from git
113
+ env:
114
+ TESTPYPI: ${{ inputs.testpypi }}
115
+ run: |
116
+ # An untagged build's version carries a local segment (`+<sha>`),
117
+ # which PyPI and TestPyPI both reject on upload, so drop it when the
118
+ # run is going to upload. A tagged build has no such segment.
119
+ style=(--style semver)
120
+ if [ "$TESTPYPI" = "true" ]; then style+=(--no-metadata); fi
121
+ version="$(uv tool run dunamai from git "${style[@]}")"
122
+ echo "Package version: $version"
123
+ # A tagged build is a release: its version has to track the gix it
124
+ # links, or the number stops meaning what RELEASING.md says it does.
125
+ if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
126
+ uv run --no-project python scripts/check_version_tracks_gix.py "$version"
127
+ fi
128
+ sed -i "0,/^version = \".*\"/s//version = \"$version\"/" Cargo.toml
129
+
130
+ - name: uv sync
131
+ run: uv sync --locked
132
+
133
+ # --generate-stubs bundles the .pyi and py.typed into the wheel; without
134
+ # it the package ships untyped and the Typing :: Typed classifier lies.
135
+ # maturin extracts the stub from the built binary rather than importing
136
+ # it, which is why it works for the cross-compiled target too.
137
+ - name: Build Linux wheel
138
+ run: uv run maturin build --release --out /dist --generate-stubs
139
+
140
+ - name: Build Windows wheel
141
+ run: |
142
+ uv run maturin build --release --target x86_64-pc-windows-gnu \
143
+ --out /dist --generate-stubs
144
+
145
+ - name: Build sdist
146
+ run: uv tool run maturin sdist --out /dist
147
+
148
+ - name: Check the Linux wheel is installable and typed
149
+ run: |
150
+ uv venv /tmp/wheel-check
151
+ uv pip install --python /tmp/wheel-check/bin/python \
152
+ --no-index --find-links /dist gitoxide-py
153
+ /tmp/wheel-check/bin/python -c "
154
+ import gitoxide_py, pathlib
155
+ pkg = pathlib.Path(gitoxide_py.__file__).parent
156
+ assert (pkg / 'py.typed').is_file(), 'py.typed missing from the wheel'
157
+ assert (pkg / '__init__.pyi').is_file(), 'stub missing from the wheel'
158
+ print('wheel ok:', gitoxide_py.open('.').head_id)
159
+ "
160
+
161
+ # Travels with the wheels so the Wine job needs no checkout of its own:
162
+ # that image has no git, and the only thing it needs from the repository
163
+ # is this script.
164
+ - name: Stage the Windows check
165
+ run: cp .github/windows-check/verify-wheel.sh /dist/
166
+
167
+ # A real Windows CPython, under Wine, importing the wheel that will be
168
+ # published -- the check a GitHub-hosted windows runner never performed,
169
+ # since it uploaded a wheel it had never loaded.
170
+ windows-check:
171
+ name: Windows wheel under Wine
172
+ needs: build
173
+ runs-on: [self-hosted, feguitv]
174
+ timeout-minutes: 20
175
+ container:
176
+ image: localhost:5000/windows-ci:latest
177
+ volumes:
178
+ - /var/tmp/gitoxide-py-release/${{ github.run_id }}:/dist
179
+ defaults:
180
+ run:
181
+ shell: bash
182
+ steps:
183
+ - name: Import the Windows wheel
184
+ run: bash /dist/verify-wheel.sh
185
+
186
+ # The same upload the real release performs, against TestPyPI, so the parts
187
+ # a tag would otherwise try for the first time -- the OIDC exchange and the
188
+ # upload -- have been exercised before one is pushed. Needs its own trusted
189
+ # publisher registered on test.pypi.org, with environment `testpypi`.
190
+ publish-testpypi:
191
+ name: Publish to TestPyPI
192
+ needs: [build, windows-check]
193
+ if: inputs.testpypi
194
+ runs-on: [self-hosted, feguitv]
195
+ timeout-minutes: 20
196
+ container:
197
+ image: localhost:5000/python-ci:latest
198
+ volumes:
199
+ - /var/tmp/gitoxide-py-release/${{ github.run_id }}:/dist
200
+ defaults:
201
+ run:
202
+ shell: bash
203
+ environment: testpypi
204
+ permissions:
205
+ # A job-level block replaces the workflow-level one rather than adding to
206
+ # it, so `contents: read` has to be repeated or the checkout fails.
207
+ contents: read
208
+ id-token: write
209
+ steps:
210
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
211
+
212
+ - name: Upload
213
+ run: uv run --no-project python scripts/publish.py https://test.pypi.org /dist
214
+
215
+ publish:
216
+ name: Publish to PyPI
217
+ needs: [build, windows-check, wheels-hosted]
218
+ # `wheels-hosted` is skipped unless HOSTED_RUNNERS is set, and a skipped
219
+ # job would otherwise skip this one too; a *failed* one still blocks.
220
+ if: |
221
+ always() && !cancelled()
222
+ && startsWith(github.ref, 'refs/tags/v')
223
+ && needs.build.result == 'success'
224
+ && needs.windows-check.result == 'success'
225
+ && needs.wheels-hosted.result != 'failure'
226
+ runs-on: [self-hosted, feguitv]
227
+ timeout-minutes: 20
228
+ container:
229
+ image: localhost:5000/python-ci:latest
230
+ volumes:
231
+ - /var/tmp/gitoxide-py-release/${{ github.run_id }}:/dist
232
+ defaults:
233
+ run:
234
+ shell: bash
235
+ environment: pypi
236
+ permissions:
237
+ contents: read
238
+ # Trusted Publishing: PyPI verifies this workflow's OIDC identity, so
239
+ # there is no API token to store or rotate. Configure the publisher at
240
+ # https://pypi.org/manage/project/gitoxide-py/settings/publishing/
241
+ id-token: write
242
+ steps:
243
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
244
+
245
+ - name: Collect the hosted wheels
246
+ if: vars.HOSTED_RUNNERS == 'true'
247
+ uses: actions/download-artifact@v4
248
+ with:
249
+ pattern: wheels-*
250
+ merge-multiple: true
251
+ path: /dist
252
+
253
+ - name: Upload
254
+ run: uv run --no-project python scripts/publish.py https://pypi.org /dist
255
+
256
+ # The shared directory lives on the runner, outside any workspace, so
257
+ # nothing else reclaims it.
258
+ cleanup:
259
+ name: Clean up
260
+ needs: [build, windows-check, publish, publish-testpypi]
261
+ if: always()
262
+ runs-on: [self-hosted, feguitv]
263
+ timeout-minutes: 10
264
+ container:
265
+ image: localhost:5000/python-ci:latest
266
+ volumes:
267
+ - /var/tmp/gitoxide-py-release/${{ github.run_id }}:/dist
268
+ defaults:
269
+ run:
270
+ shell: bash
271
+ steps:
272
+ - name: Empty the shared directory
273
+ run: rm -rf /dist/* /dist/.[!.]* 2>/dev/null || true
@@ -0,0 +1,8 @@
1
+ /target
2
+ /.venv
3
+ __pycache__/
4
+ *.so
5
+ *.pyd
6
+ /dist
7
+ /*.egg-info
8
+ *.profraw