Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Release

# v* tag push: build wheel, sdist, and Windows zip; publish attaches all three to the Release.
# workflow_dispatch runs the build jobs only (no publish).
#
# Asset names (version from pyproject.toml at the tagged commit):
# cppa_cursor_browser-<version>-py3-none-any.whl (~154 KiB at 0.2.0)
# cppa_cursor_browser-<version>.tar.gz (~225 KiB at 0.2.0)
# CursorChatBrowser-windows.zip
#
# Fork rehearsal: push v0.2.0 (pyproject is already 0.2.0) and confirm all three assets on the Release.

on:
push:
tags:
- "v*"
Comment thread
clean6378-max-it marked this conversation as resolved.
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build-python:
name: Build wheel and sdist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Tag must match pyproject version
if: github.ref_type == 'tag'
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
tag_version="${RELEASE_TAG#v}"
pyproject_version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
if [ "$tag_version" != "$pyproject_version" ]; then
echo "Release tag is $tag_version but pyproject.toml [project].version is $pyproject_version"
exit 1
fi

- name: Build hatchling distributables
run: |
python -m pip install --upgrade pip
python -m pip install 'build>=1,<2'
python -m build

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: python-distributables
path: |
dist/*.whl
dist/*.tar.gz
if-no-files-found: error

build-windows:
name: Build Windows PyInstaller bundle
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Install runtime dependencies
# Runtime from requirements-lock.txt; pywebview pin from pyproject [desktop].
shell: pwsh
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-lock.txt
$spec = python scripts/read_desktop_pywebview_spec.py
python -m pip install $spec

- name: Install PyInstaller
run: python -m pip install 'pyinstaller>=6,<7'

- name: Build PyInstaller bundle
run: pyinstaller cursor-browser.spec --noconfirm

- name: Check webview/lib in bundle
shell: pwsh
run: |
$lib = 'dist\CursorChatBrowser\_internal\webview\lib'
if (-not (Test-Path $lib)) {
Write-Error "pywebview bundle missing: $lib"
exit 1
}

- name: Zip onedir bundle
shell: pwsh
run: |
if (-not (Test-Path dist\CursorChatBrowser\CursorChatBrowser.exe)) {
Write-Error "dist\CursorChatBrowser\CursorChatBrowser.exe not found"
exit 1
}
Compress-Archive -Path dist\CursorChatBrowser -DestinationPath CursorChatBrowser-windows.zip

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: windows-bundle
path: CursorChatBrowser-windows.zip
if-no-files-found: error

publish:
name: Publish GitHub Release assets
needs: [build-python, build-windows]
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download Python distributables
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-distributables
path: release-assets

- name: Download Windows bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: windows-bundle
path: release-assets

- name: Verify release asset set
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
ls -la release-assets/
count=$(find release-assets -maxdepth 1 -type f | wc -l)
test "$count" -eq 3
test -f "release-assets/cppa_cursor_browser-${version}-py3-none-any.whl"
test -f "release-assets/cppa_cursor_browser-${version}.tar.gz"
test -f release-assets/CursorChatBrowser-windows.zip
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Attach artifacts to Release
Comment thread
clean6378-max-it marked this conversation as resolved.
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: release-assets/*
25 changes: 21 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ jobs:
sys.exit(1)
PY

- name: Check pywebview pin is single-sourced
run: |
python scripts/read_desktop_pywebview_spec.py
python scripts/check_pywebview_workflow_pin.py

- name: Install pip-tools
# Pin matches update-lock.yml so lock verification uses the same resolver.
run: python -m pip install 'pip-tools==7.5.3'
Expand Down Expand Up @@ -118,8 +123,14 @@ jobs:
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=

# ── PyInstaller desktop build (Windows only, once per workflow) ────────
# Closes #44. Builds the onedir bundle and smoke-tests --help so the
# desktop entry point is verified without launching the GUI window.
# Closes #44. Check webview/lib after build; --help returns before import webview.
- name: Install pywebview for PyInstaller bundle
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
shell: pwsh
run: |
$spec = python scripts/read_desktop_pywebview_spec.py
python -m pip install $spec

- name: Install PyInstaller
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: python -m pip install 'pyinstaller>=6,<7'
Expand All @@ -128,9 +139,15 @@ jobs:
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: pyinstaller cursor-browser.spec --noconfirm

- name: Smoke-test PyInstaller exe (--help)
- name: Check webview/lib in bundle
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: dist\CursorChatBrowser\CursorChatBrowser.exe --help
shell: pwsh
run: |
$lib = 'dist\CursorChatBrowser\_internal\webview\lib'
if (-not (Test-Path $lib)) {
Write-Error "pywebview bundle missing: $lib"
exit 1
}

# ── Browser XSS: Playwright (sprint item #3) ─────────────────────────────
browser-xss:
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ Adding new optional fields to JSON responses, adding new CLI flags with sensible

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

### GitHub Releases

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.

| Asset | Contents |
|---|---|
| `cppa_cursor_browser-<version>-py3-none-any.whl` | Installable wheel (hatchling build) |
| `cppa_cursor_browser-<version>.tar.gz` | Source distribution |
| `CursorChatBrowser-windows.zip` | Windows PyInstaller onedir bundle (`CursorChatBrowser.exe` plus supporting files) |

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`.

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).

## License
Expand Down
1 change: 1 addition & 0 deletions cursor-browser.spec
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ a = Analysis(
(str(src / "static"), "static"),
],
hiddenimports=[
"webview", # needs pywebview installed at build time
"api.workspaces",
"api.composers",
"api.logs",
Expand Down
31 changes: 31 additions & 0 deletions scripts/check_pywebview_workflow_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Fail if workflow YAML hardcodes a pywebview version instead of read_desktop_pywebview_spec."""

from __future__ import annotations

import sys
from pathlib import Path

READ_SCRIPT = "read_desktop_pywebview_spec.py"
PYWEBVIEW_INSTALL_WORKFLOWS = ("release.yml", "tests.yml")


def main() -> None:
for path in sorted(Path(".github/workflows").glob("*.yml")):
text = path.read_text()
if "pywebview>=" in text or "pywebview<" in text:
print(
f"{path} hardcodes a pywebview version; use scripts/{READ_SCRIPT}",
file=sys.stderr,
)
raise SystemExit(1)
if READ_SCRIPT not in text and "pywebview" in text.lower():
if path.name in PYWEBVIEW_INSTALL_WORKFLOWS:
print(
f"{path} installs pywebview but does not read the pin from pyproject",
file=sys.stderr,
)
raise SystemExit(1)


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions scripts/read_desktop_pywebview_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Stdout the pywebview pin from pyproject.toml [desktop]."""

from __future__ import annotations

import sys
import tomllib


def main() -> None:
deps = tomllib.load(open("pyproject.toml", "rb"))["project"]["optional-dependencies"]["desktop"]
if len(deps) != 1 or not deps[0].startswith("pywebview"):
print(
"need exactly one pywebview dep in [project.optional-dependencies].desktop",
file=sys.stderr,
)
raise SystemExit(1)
print(deps[0])


if __name__ == "__main__":
main()
Loading