Skip to content

Commit afc21e4

Browse files
feat: tag-triggered release workflow for wheel, sdist and Windows bundle (#159)
* feat: add tag-triggered release workflow for wheel, sdist and Windows bundle * ci: limit release contents write to publish job and fix tag docs * Address brad's feedback * add whl, tar.gz test more * clean up content * ci: enforce tag version match and pywebview bundle checks * moving guard from inline in tests.yml
1 parent 2ded4bc commit afc21e4

6 files changed

Lines changed: 241 additions & 4 deletions

File tree

.github/workflows/release.yml

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: Release
2+
3+
# v* tag push: build wheel, sdist, and Windows zip; publish attaches all three to the Release.
4+
# workflow_dispatch runs the build jobs only (no publish).
5+
#
6+
# Asset names (version from pyproject.toml at the tagged commit):
7+
# cppa_cursor_browser-<version>-py3-none-any.whl (~154 KiB at 0.2.0)
8+
# cppa_cursor_browser-<version>.tar.gz (~225 KiB at 0.2.0)
9+
# CursorChatBrowser-windows.zip
10+
#
11+
# Fork rehearsal: push v0.2.0 (pyproject is already 0.2.0) and confirm all three assets on the Release.
12+
13+
on:
14+
push:
15+
tags:
16+
- "v*"
17+
workflow_dispatch:
18+
19+
permissions:
20+
contents: read
21+
22+
concurrency:
23+
group: release-${{ github.ref }}
24+
cancel-in-progress: false
25+
26+
jobs:
27+
build-python:
28+
name: Build wheel and sdist
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
32+
with:
33+
persist-credentials: false
34+
35+
- name: Set up Python
36+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
37+
with:
38+
python-version: "3.12"
39+
40+
- name: Tag must match pyproject version
41+
if: github.ref_type == 'tag'
42+
env:
43+
RELEASE_TAG: ${{ github.ref_name }}
44+
run: |
45+
set -euo pipefail
46+
tag_version="${RELEASE_TAG#v}"
47+
pyproject_version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
48+
if [ "$tag_version" != "$pyproject_version" ]; then
49+
echo "Release tag is $tag_version but pyproject.toml [project].version is $pyproject_version"
50+
exit 1
51+
fi
52+
53+
- name: Build hatchling distributables
54+
run: |
55+
python -m pip install --upgrade pip
56+
python -m pip install 'build>=1,<2'
57+
python -m build
58+
59+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
60+
with:
61+
name: python-distributables
62+
path: |
63+
dist/*.whl
64+
dist/*.tar.gz
65+
if-no-files-found: error
66+
67+
build-windows:
68+
name: Build Windows PyInstaller bundle
69+
runs-on: windows-latest
70+
steps:
71+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
72+
with:
73+
persist-credentials: false
74+
75+
- name: Set up Python
76+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
77+
with:
78+
python-version: "3.12"
79+
80+
- name: Install runtime dependencies
81+
# Runtime from requirements-lock.txt; pywebview pin from pyproject [desktop].
82+
shell: pwsh
83+
run: |
84+
python -m pip install --upgrade pip
85+
python -m pip install -r requirements-lock.txt
86+
$spec = python scripts/read_desktop_pywebview_spec.py
87+
python -m pip install $spec
88+
89+
- name: Install PyInstaller
90+
run: python -m pip install 'pyinstaller>=6,<7'
91+
92+
- name: Build PyInstaller bundle
93+
run: pyinstaller cursor-browser.spec --noconfirm
94+
95+
- name: Check webview/lib in bundle
96+
shell: pwsh
97+
run: |
98+
$lib = 'dist\CursorChatBrowser\_internal\webview\lib'
99+
if (-not (Test-Path $lib)) {
100+
Write-Error "pywebview bundle missing: $lib"
101+
exit 1
102+
}
103+
104+
- name: Zip onedir bundle
105+
shell: pwsh
106+
run: |
107+
if (-not (Test-Path dist\CursorChatBrowser\CursorChatBrowser.exe)) {
108+
Write-Error "dist\CursorChatBrowser\CursorChatBrowser.exe not found"
109+
exit 1
110+
}
111+
Compress-Archive -Path dist\CursorChatBrowser -DestinationPath CursorChatBrowser-windows.zip
112+
113+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
114+
with:
115+
name: windows-bundle
116+
path: CursorChatBrowser-windows.zip
117+
if-no-files-found: error
118+
119+
publish:
120+
name: Publish GitHub Release assets
121+
needs: [build-python, build-windows]
122+
if: github.event_name == 'push' && github.ref_type == 'tag'
123+
runs-on: ubuntu-latest
124+
permissions:
125+
contents: write
126+
steps:
127+
- name: Download Python distributables
128+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
129+
with:
130+
name: python-distributables
131+
path: release-assets
132+
133+
- name: Download Windows bundle
134+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
135+
with:
136+
name: windows-bundle
137+
path: release-assets
138+
139+
- name: Verify release asset set
140+
env:
141+
RELEASE_TAG: ${{ github.ref_name }}
142+
run: |
143+
set -euo pipefail
144+
version="${RELEASE_TAG#v}"
145+
ls -la release-assets/
146+
count=$(find release-assets -maxdepth 1 -type f | wc -l)
147+
test "$count" -eq 3
148+
test -f "release-assets/cppa_cursor_browser-${version}-py3-none-any.whl"
149+
test -f "release-assets/cppa_cursor_browser-${version}.tar.gz"
150+
test -f release-assets/CursorChatBrowser-windows.zip
151+
152+
- name: Attach artifacts to Release
153+
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
154+
with:
155+
files: release-assets/*

.github/workflows/tests.yml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ jobs:
5454
sys.exit(1)
5555
PY
5656
57+
- name: Check pywebview pin is single-sourced
58+
run: |
59+
python scripts/read_desktop_pywebview_spec.py
60+
python scripts/check_pywebview_workflow_pin.py
61+
5762
- name: Install pip-tools
5863
# Pin matches update-lock.yml so lock verification uses the same resolver.
5964
run: python -m pip install 'pip-tools==7.5.3'
@@ -118,8 +123,14 @@ jobs:
118123
run: python -m pytest tests/test_api_search.py tests/test_api_workspaces.py tests/test_api_export.py tests/test_pdf_export.py tests/test_search_helpers.py tests/test_check_benchmark_regression.py tests/test_reduce_baselines.py -v --tb=short -o addopts=
119124

120125
# ── PyInstaller desktop build (Windows only, once per workflow) ────────
121-
# Closes #44. Builds the onedir bundle and smoke-tests --help so the
122-
# desktop entry point is verified without launching the GUI window.
126+
# Closes #44. Check webview/lib after build; --help returns before import webview.
127+
- name: Install pywebview for PyInstaller bundle
128+
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
129+
shell: pwsh
130+
run: |
131+
$spec = python scripts/read_desktop_pywebview_spec.py
132+
python -m pip install $spec
133+
123134
- name: Install PyInstaller
124135
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
125136
run: python -m pip install 'pyinstaller>=6,<7'
@@ -128,9 +139,15 @@ jobs:
128139
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
129140
run: pyinstaller cursor-browser.spec --noconfirm
130141

131-
- name: Smoke-test PyInstaller exe (--help)
142+
- name: Check webview/lib in bundle
132143
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
133-
run: dist\CursorChatBrowser\CursorChatBrowser.exe --help
144+
shell: pwsh
145+
run: |
146+
$lib = 'dist\CursorChatBrowser\_internal\webview\lib'
147+
if (-not (Test-Path $lib)) {
148+
Write-Error "pywebview bundle missing: $lib"
149+
exit 1
150+
}
134151
135152
# ── Browser XSS: Playwright (sprint item #3) ─────────────────────────────
136153
browser-xss:

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,18 @@ Adding new optional fields to JSON responses, adding new CLI flags with sensible
263263

264264
Notable changes will be documented in **[CHANGELOG.md](CHANGELOG.md)** following the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
265265

266+
### GitHub Releases
267+
268+
Push a `v*` tag (for example `v0.2.1`) to run [`.github/workflows/release.yml`](.github/workflows/release.yml). It uploads three assets to the GitHub Release. Set `[project].version` in `pyproject.toml` to match the tag before you push (`v0.2.1` needs `version = "0.2.1"`). Hatchling names the wheel and sdist from pyproject, not the git tag.
269+
270+
| Asset | Contents |
271+
|---|---|
272+
| `cppa_cursor_browser-<version>-py3-none-any.whl` | Installable wheel (hatchling build) |
273+
| `cppa_cursor_browser-<version>.tar.gz` | Source distribution |
274+
| `CursorChatBrowser-windows.zip` | Windows PyInstaller onedir bundle (`CursorChatBrowser.exe` plus supporting files) |
275+
276+
At `0.2.0`, a local `python -m build` gave a ~154 KiB wheel and ~225 KiB sdist. Windows zip size varies with the locked tree at tag time. Paste release notes from the matching `[version]` section in `CHANGELOG.md`.
277+
266278
When an API surface is scheduled for removal, follow the process in **[docs/API_DEPRECATION.md](docs/API_DEPRECATION.md)** (response headers, changelog entries, minimum notice period).
267279

268280
## License

cursor-browser.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ a = Analysis(
1717
(str(src / "static"), "static"),
1818
],
1919
hiddenimports=[
20+
"webview", # needs pywebview installed at build time
2021
"api.workspaces",
2122
"api.composers",
2223
"api.logs",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Fail if workflow YAML hardcodes a pywebview version instead of read_desktop_pywebview_spec."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
READ_SCRIPT = "read_desktop_pywebview_spec.py"
9+
PYWEBVIEW_INSTALL_WORKFLOWS = ("release.yml", "tests.yml")
10+
11+
12+
def main() -> None:
13+
for path in sorted(Path(".github/workflows").glob("*.yml")):
14+
text = path.read_text()
15+
if "pywebview>=" in text or "pywebview<" in text:
16+
print(
17+
f"{path} hardcodes a pywebview version; use scripts/{READ_SCRIPT}",
18+
file=sys.stderr,
19+
)
20+
raise SystemExit(1)
21+
if READ_SCRIPT not in text and "pywebview" in text.lower():
22+
if path.name in PYWEBVIEW_INSTALL_WORKFLOWS:
23+
print(
24+
f"{path} installs pywebview but does not read the pin from pyproject",
25+
file=sys.stderr,
26+
)
27+
raise SystemExit(1)
28+
29+
30+
if __name__ == "__main__":
31+
main()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Stdout the pywebview pin from pyproject.toml [desktop]."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
import tomllib
7+
8+
9+
def main() -> None:
10+
deps = tomllib.load(open("pyproject.toml", "rb"))["project"]["optional-dependencies"]["desktop"]
11+
if len(deps) != 1 or not deps[0].startswith("pywebview"):
12+
print(
13+
"need exactly one pywebview dep in [project.optional-dependencies].desktop",
14+
file=sys.stderr,
15+
)
16+
raise SystemExit(1)
17+
print(deps[0])
18+
19+
20+
if __name__ == "__main__":
21+
main()

0 commit comments

Comments
 (0)