Skip to content

Commit 7ddac38

Browse files
committed
Move releases to immutable tags with Trusted Publishing and add CI gates
Publishing on every push to main made releases unauditable. Releases now require a v* tag whose version must match pyproject, build provenance is attested, and PyPI uses Trusted Publishing. quality.yml gains dependency review, a coverage floor, and a mypy gate on the stable API surface; a platform-smoke matrix exercises the stable API on all three OSes.
1 parent 843fc4b commit 7ddac38

7 files changed

Lines changed: 172 additions & 2 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Platform smoke
2+
3+
on:
4+
push:
5+
branches: ["main", "dev"]
6+
pull_request:
7+
branches: ["main", "dev"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
stable-api:
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [windows-2022, ubuntu-22.04, macos-14]
18+
python-version: ["3.10", "3.14"]
19+
runs-on: ${{ matrix.os }}
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
- run: python -m pip install -e .
26+
- name: Import stable API and generate platform-neutral code
27+
run: >-
28+
python -c "import je_auto_control.api as ac;
29+
compile(ac.generate_code([['AC_screen_size']], style='actions'),
30+
'<generated>', 'exec')"
31+
- name: Create headless diagnostic bundle
32+
run: >-
33+
python -c "from je_auto_control.api import
34+
FailureBundleOptions, create_failure_bundle;
35+
create_failure_bundle('platform-smoke.zip',
36+
options=FailureBundleOptions(screenshot=False))"
37+
- uses: actions/upload-artifact@v4
38+
if: always()
39+
with:
40+
name: platform-smoke-${{ matrix.os }}-${{ matrix.python-version }}
41+
path: platform-smoke.zip
42+
if-no-files-found: warn

.github/workflows/quality.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ permissions:
1616
contents: read
1717

1818
jobs:
19+
dependency-review:
20+
if: github.event_name == 'pull_request'
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: actions/dependency-review-action@v4
27+
1928
lint:
2029
runs-on: ubuntu-latest
2130
steps:
@@ -84,4 +93,23 @@ jobs:
8493
pip install ruff==0.15.14 bandit==1.9.4 pytest==9.0.3 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 PySide6==6.11.1
8594
8695
- name: Run headless pytest suite
87-
run: pytest test/unit_test/headless/ -v --tb=short --timeout=120
96+
run: >-
97+
pytest test/unit_test/headless/ -v --tb=short --timeout=120
98+
--cov=je_auto_control --cov-report=term-missing
99+
--cov-report=xml --cov-fail-under=35
100+
101+
- name: Upload coverage report
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: coverage-${{ matrix.python-version }}
105+
path: coverage.xml
106+
107+
typing-stable-api:
108+
runs-on: ubuntu-latest
109+
steps:
110+
- uses: actions/checkout@v4
111+
- uses: actions/setup-python@v5
112+
with:
113+
python-version: "3.12"
114+
- run: pip install -e . mypy
115+
- run: mypy je_auto_control/api je_auto_control/utils/failure_bundle

.github/workflows/release.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: read
15+
id-token: write
16+
attestations: write
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
- run: python -m pip install --upgrade build twine
23+
- name: Verify tag matches package version
24+
env:
25+
RELEASE_TAG: ${{ github.ref_name }}
26+
run: |
27+
python - <<'PY'
28+
import os, tomllib
29+
with open("pyproject.toml", "rb") as handle:
30+
version = tomllib.load(handle)["project"]["version"]
31+
if os.environ["RELEASE_TAG"] != f"v{version}":
32+
raise SystemExit(f"tag {os.environ['RELEASE_TAG']} != v{version}")
33+
PY
34+
- run: python -m build
35+
- run: python -m twine check dist/*
36+
- name: Smoke-test the built wheel
37+
run: |
38+
python -m venv /tmp/wheel-test
39+
/tmp/wheel-test/bin/pip install dist/*.whl
40+
/tmp/wheel-test/bin/python -c "import je_auto_control.api"
41+
- uses: actions/attest-build-provenance@v2
42+
with:
43+
subject-path: "dist/*"
44+
- uses: actions/upload-artifact@v4
45+
with:
46+
name: python-distributions
47+
path: dist/
48+
49+
publish:
50+
needs: build
51+
runs-on: ubuntu-latest
52+
environment:
53+
name: pypi
54+
url: https://pypi.org/p/je-auto-control
55+
permissions:
56+
id-token: write
57+
steps:
58+
- uses: actions/download-artifact@v4
59+
with:
60+
name: python-distributions
61+
path: dist/
62+
- uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/stable.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ jobs:
118118

119119
publish:
120120
needs: test
121-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
121+
# Publishing moved to release.yml: immutable v* tags + Trusted Publishing.
122+
if: ${{ false }}
122123
runs-on: ubuntu-latest
123124
permissions:
124125
contents: write

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,8 @@ dmypy.json
126126
/.claude/
127127
/.claude
128128
/.idea
129+
130+
# Local test/smoke artifacts
131+
.test-tmp/
132+
bundle-smoke.zip
133+
platform-smoke.zip

dev_requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ bandit==1.9.4
2020
pytest==9.0.3
2121
pytest-timeout==2.4.0
2222
pytest-rerunfailures==15.1
23+
pytest-cov>=6.0
24+
mypy>=1.15

pyproject.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,33 @@ exclude_dirs = [
8888
# B101 (use of assert) — pytest test code intentionally uses assert.
8989
# Library code is enforced by CLAUDE.md (no assert in non-test code).
9090
skips = ["B101"]
91+
92+
[tool.pytest.ini_options]
93+
testpaths = ["test/unit_test/headless"]
94+
addopts = "--strict-markers --strict-config"
95+
96+
[tool.coverage.run]
97+
branch = true
98+
source = ["je_auto_control"]
99+
omit = ["*/gui/*", "*/language_wrapper/*"]
100+
101+
[tool.coverage.report]
102+
show_missing = true
103+
skip_covered = true
104+
# Initial measured repository baseline. Raise toward 70 as legacy modules are
105+
# brought under the stable API contract; CI enforces that it cannot regress.
106+
fail_under = 35
107+
108+
[tool.mypy]
109+
python_version = "3.10"
110+
warn_redundant_casts = true
111+
check_untyped_defs = true
112+
no_implicit_optional = true
113+
# CI type-checks only the stable API surface; followed legacy modules are
114+
# analysed for signatures but not reported until they join the contract.
115+
follow_imports = "silent"
116+
exclude = "(^test/|^docs/|^build/)"
117+
118+
[[tool.mypy.overrides]]
119+
module = ["cv2.*", "Xlib.*", "PySide6.*", "objc.*"]
120+
ignore_missing_imports = true

0 commit comments

Comments
 (0)