Skip to content

Commit 98ba63c

Browse files
committed
Prepare PQViewer3D releases
1 parent 68cec6c commit 98ba63c

11 files changed

Lines changed: 457 additions & 36 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,7 @@ jobs:
2727
run: |
2828
wheel=$(find dist -name '*.whl' -print -quit)
2929
sdist=$(find dist -name '*.tar.gz' -print -quit)
30-
python - "$wheel" <<'PY'
31-
import re
32-
import sys
33-
from zipfile import ZipFile
34-
35-
with ZipFile(sys.argv[1]) as archive:
36-
files = set(archive.namelist())
37-
index = "pqviewer/static/index.html"
38-
assert index in files, f"missing {index}"
39-
html = archive.read(index).decode()
40-
for asset in re.findall(r'(?:src|href)="(/assets/[^"?]+)', html):
41-
bundled = f"pqviewer/static{asset}"
42-
assert bundled in files, f"missing {bundled}"
43-
if asset.endswith(".css"):
44-
css = archive.read(bundled).decode()
45-
for nested in re.findall(r"url\((/assets/[^)]+)\)", css):
46-
nested_file = f"pqviewer/static{nested}"
47-
assert nested_file in files, f"missing {nested_file}"
48-
PY
49-
tar -tzf "$sdist" | grep -q '/docs/conf.py$'
30+
python scripts/verify_distribution.py "$wheel" "$sdist"
5031
python -m venv /tmp/pqviewer-smoke
5132
/tmp/pqviewer-smoke/bin/pip install "$wheel"
5233
cd /tmp

.github/workflows/release.yml

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: release-${{ github.ref }}
13+
cancel-in-progress: false
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-24.04
18+
steps:
19+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20+
with:
21+
fetch-depth: 0
22+
persist-credentials: false
23+
- name: Verify release commit
24+
run: |
25+
git fetch --no-tags origin main:refs/remotes/origin/main
26+
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
27+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
28+
with:
29+
python-version: "3.12"
30+
cache: pip
31+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
32+
with:
33+
node-version: "24"
34+
cache: npm
35+
cache-dependency-path: frontend/package-lock.json
36+
- name: Verify release version
37+
env:
38+
RELEASE_TAG: ${{ github.ref_name }}
39+
run: |
40+
python - "$RELEASE_TAG" <<'PY'
41+
import json
42+
import re
43+
import sys
44+
import tomllib
45+
from pathlib import Path
46+
47+
project = tomllib.loads(Path("pyproject.toml").read_text())["project"]
48+
version = project["version"]
49+
tag = sys.argv[1]
50+
tag_pattern = (
51+
r"^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)"
52+
r"(?:(?:a|b|rc)(?:0|[1-9]\d*))?$"
53+
)
54+
if not re.fullmatch(tag_pattern, tag):
55+
raise SystemExit(f"tag {tag} is not a supported release tag")
56+
if project["name"] != "PQViewer3D":
57+
raise SystemExit("unexpected Python distribution name")
58+
if tag != f"v{version}":
59+
raise SystemExit(f"tag {tag} does not match version {version}")
60+
61+
frontend = json.loads(Path("frontend/package.json").read_text())["version"]
62+
lockfile = json.loads(Path("frontend/package-lock.json").read_text())
63+
citation = Path("CITATION.cff").read_text()
64+
changelog = Path("CHANGELOG.md").read_text()
65+
if (
66+
frontend != version
67+
or lockfile["version"] != version
68+
or lockfile["packages"][""]["version"] != version
69+
):
70+
raise SystemExit("frontend version does not match the release")
71+
if not re.search(rf"^version: {re.escape(version)}$", citation, re.MULTILINE):
72+
raise SystemExit("CITATION.cff version does not match the release")
73+
citation_date = re.search(
74+
r'^date-released: ["\']?(\d{4}-\d{2}-\d{2})["\']?$',
75+
citation,
76+
re.MULTILINE,
77+
)
78+
changelog_date = re.search(
79+
rf"^## \[{re.escape(version)}\] - (\d{{4}}-\d{{2}}-\d{{2}})$",
80+
changelog,
81+
re.MULTILINE,
82+
)
83+
if citation_date is None or changelog_date is None:
84+
raise SystemExit("release date is missing from CITATION.cff or CHANGELOG.md")
85+
if citation_date.group(1) != changelog_date.group(1):
86+
raise SystemExit("release dates do not match")
87+
PY
88+
- name: Install Python dependencies
89+
run: python -m pip install --upgrade ".[dev]" build twine
90+
- name: Run Python tests
91+
run: python -m pytest
92+
- name: Build bundled interface
93+
working-directory: frontend
94+
run: |
95+
npm ci
96+
npm test
97+
npm run build
98+
- name: Verify bundled interface
99+
run: git diff --exit-code -- pqviewer/static
100+
- name: Build Python distributions
101+
run: |
102+
python -m build
103+
python -m twine check --strict dist/*
104+
wheel=$(find dist -name '*.whl' -print -quit)
105+
sdist=$(find dist -name '*.tar.gz' -print -quit)
106+
python scripts/verify_distribution.py "$wheel" "$sdist"
107+
- name: Upload Python distributions
108+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
109+
with:
110+
name: python-distributions
111+
path: dist/
112+
if-no-files-found: error
113+
retention-days: 14
114+
115+
smoke:
116+
needs: build
117+
strategy:
118+
matrix:
119+
os:
120+
- ubuntu-24.04
121+
- macos-15
122+
runs-on: ${{ matrix.os }}
123+
steps:
124+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
125+
with:
126+
python-version: "3.12"
127+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
128+
with:
129+
name: python-distributions
130+
path: dist
131+
- name: Install wheel
132+
run: |
133+
wheel=$(find dist -name '*.whl' -print -quit)
134+
python -m pip install "$wheel"
135+
- name: Verify installed application
136+
run: |
137+
python -m pip check
138+
python - <<'PY'
139+
from importlib.resources import files
140+
141+
import pqviewer
142+
143+
assert (files("pqviewer") / "static" / "index.html").is_file()
144+
assert pqviewer.__name__ == "pqviewer"
145+
PY
146+
pqviewer --help
147+
pqviewer render --help
148+
pqviewer-render --help
149+
- name: Smoke-test local server
150+
run: |
151+
pqviewer --no-open --port 8765 > pqviewer.log 2>&1 &
152+
server_pid=$!
153+
trap 'kill "$server_pid" 2>/dev/null || true' EXIT
154+
for attempt in {1..30}; do
155+
if curl --fail --silent http://127.0.0.1:8765/api/health > /dev/null; then
156+
break
157+
fi
158+
sleep 1
159+
done
160+
if ! curl --fail --silent http://127.0.0.1:8765/api/health; then
161+
cat pqviewer.log
162+
exit 1
163+
fi
164+
curl --fail --silent http://127.0.0.1:8765/ > /dev/null
165+
kill "$server_pid" 2>/dev/null || true
166+
wait "$server_pid" || true
167+
trap - EXIT
168+
169+
browser:
170+
runs-on: ubuntu-24.04
171+
timeout-minutes: 30
172+
steps:
173+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
174+
with:
175+
persist-credentials: false
176+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
177+
with:
178+
python-version: "3.12"
179+
cache: pip
180+
- name: Install Python dependencies
181+
run: python -m pip install ".[dev,render]"
182+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
183+
with:
184+
node-version: "24"
185+
cache: npm
186+
cache-dependency-path: frontend/package-lock.json
187+
- name: Install browser dependencies
188+
working-directory: frontend
189+
run: |
190+
npm ci
191+
npx playwright install --with-deps chromium
192+
- name: Install render browser
193+
run: python -m playwright install chromium
194+
- name: Run browser tests
195+
working-directory: frontend
196+
run: npm run test:e2e
197+
- name: Smoke-test headless rendering
198+
env:
199+
PQVIEWER_HEADLESS_TEST: "1"
200+
run: python -m pytest tests/test_recipe.py -k headless_render_command
201+
- name: Upload browser artifacts
202+
if: failure()
203+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
204+
with:
205+
name: release-browser-artifacts
206+
path: output/playwright
207+
if-no-files-found: ignore
208+
209+
publish-pypi:
210+
needs:
211+
- smoke
212+
- browser
213+
runs-on: ubuntu-latest
214+
environment:
215+
name: pypi
216+
url: https://pypi.org/p/PQViewer3D
217+
permissions:
218+
id-token: write
219+
steps:
220+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
221+
with:
222+
name: python-distributions
223+
path: dist
224+
- name: Publish to PyPI
225+
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
226+
227+
github-release:
228+
needs:
229+
- smoke
230+
- publish-pypi
231+
runs-on: ubuntu-24.04
232+
permissions:
233+
contents: write
234+
steps:
235+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
236+
with:
237+
name: python-distributions
238+
path: dist
239+
- name: Add release files
240+
env:
241+
GH_TOKEN: ${{ github.token }}
242+
RELEASE_TAG: ${{ github.ref_name }}
243+
run: |
244+
cd dist
245+
sha256sum *.whl *.tar.gz > SHA256SUMS
246+
release_flags=()
247+
if [[ "$RELEASE_TAG" =~ (a|b|rc)[0-9]+$ ]]; then
248+
release_flags+=(--prerelease)
249+
fi
250+
gh release create "$RELEASE_TAG" \
251+
./*.whl ./*.tar.gz ./SHA256SUMS \
252+
--generate-notes \
253+
--title "PQViewer ${RELEASE_TAG#v}" \
254+
--verify-tag \
255+
--repo "$GITHUB_REPOSITORY" \
256+
"${release_flags[@]}"

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,7 @@ MIT License.
8888
third-party license notices.
8989
8. Before announcing public access, enable and test private vulnerability
9090
reporting, security alerts, and dependency alerts.
91-
9. Tag the version and publish a GitHub release. Mark pre-1.0 test releases as
92-
pre-releases.
91+
9. Push the version tag and let the release workflow build and smoke-test the
92+
wheel and source archive. Use a PEP 440 pre-release version for test releases.
93+
10. Approve the protected `pypi` environment. The workflow publishes to PyPI,
94+
then creates the GitHub release with checksums.

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
<img src="frontend/public/pq-logo.png" alt="PQViewer logo" width="200">
1+
<img src="https://raw.githubusercontent.com/MolarVerse/PQViewer/main/frontend/public/pq-logo.png" alt="PQViewer logo" width="200">
22

33
# PQViewer
44

5+
[![CI](https://github.com/MolarVerse/PQViewer/actions/workflows/ci.yml/badge.svg)](https://github.com/MolarVerse/PQViewer/actions/workflows/ci.yml)
6+
[![Documentation](https://github.com/MolarVerse/PQViewer/actions/workflows/pages.yml/badge.svg)](https://molarverse.github.io/PQViewer/)
7+
[![Python 3.12+](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)](https://github.com/MolarVerse/PQViewer/blob/main/pyproject.toml)
8+
[![License: MIT](https://img.shields.io/badge/License-MIT-2f718f.svg)](https://github.com/MolarVerse/PQViewer/blob/main/LICENSE)
9+
510
PQViewer opens molecular structures and trajectories from PQ in a local
611
browser, with optional ASE format support. It provides indexed playback,
712
centred periodic cells, measurements, and reproducible figure export.
813

914
[Documentation](https://molarverse.github.io/PQViewer/) ·
1015
[Web demo](https://molarverse.github.io/PQViewer/viewer/) ·
11-
[Jupyter example](examples/pqviewer-notebook.ipynb)
16+
[Jupyter example](https://github.com/MolarVerse/PQViewer/blob/main/examples/pqviewer-notebook.ipynb)
1217

13-
![PQViewer showing a 100-frame UMCM-9 trajectory](docs/assets/screenshots/trajectory-workspace.png)
18+
![PQViewer showing a 100-frame UMCM-9 trajectory](https://raw.githubusercontent.com/MolarVerse/PQViewer/main/docs/assets/screenshots/trajectory-workspace.png)
1419

1520
PQViewer is preparing for its first public beta. File and Python interfaces may
1621
change before 1.0.
1722

1823
## Install
1924

2025
PQViewer requires Python 3.12 or newer.
26+
The Python distribution is named `PQViewer3D`; the application, import package,
27+
and command remain `PQViewer`, `pqviewer`, and `pqviewer`.
2128

2229
```bash
2330
git clone https://github.com/MolarVerse/PQViewer.git
@@ -65,9 +72,12 @@ files, ASE objects, companion data, and remote kernels.
6572

6673
## Project
6774

68-
[Contributing](CONTRIBUTING.md) · [Citation](CITATION.cff) ·
69-
[Security](SECURITY.md) · [Changelog](CHANGELOG.md) · [License](LICENSE) ·
70-
[Third-party notices](THIRD_PARTY_NOTICES.md)
75+
[Contributing](https://github.com/MolarVerse/PQViewer/blob/main/CONTRIBUTING.md) ·
76+
[Citation](https://github.com/MolarVerse/PQViewer/blob/main/CITATION.cff) ·
77+
[Security](https://github.com/MolarVerse/PQViewer/blob/main/SECURITY.md) ·
78+
[Changelog](https://github.com/MolarVerse/PQViewer/blob/main/CHANGELOG.md) ·
79+
[License](https://github.com/MolarVerse/PQViewer/blob/main/LICENSE) ·
80+
[Third-party notices](https://github.com/MolarVerse/PQViewer/blob/main/THIRD_PARTY_NOTICES.md)
7181

7282
PQViewer binds to `127.0.0.1` by default and does not provide authentication.
7383
Do not expose the local server to an untrusted network.

0 commit comments

Comments
 (0)