Release #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing release tag to publish | |
| required: true | |
| type: choice | |
| options: | |
| - v0.1.0 | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ inputs.tag || github.ref_name }} | |
| cancel-in-progress: false | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag || github.ref_name }} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ inputs.tag && format('refs/tags/{0}', inputs.tag) || github.ref }} | |
| - name: Verify release commit | |
| run: | | |
| git fetch --no-tags origin main:refs/remotes/origin/main | |
| release_sha=$(git rev-parse HEAD) | |
| tag_object=$(git rev-parse --verify "refs/tags/${RELEASE_TAG}") | |
| tag_sha=$(git rev-parse --verify "refs/tags/${RELEASE_TAG}^{commit}") | |
| test "$release_sha" = "$tag_sha" | |
| git merge-base --is-ancestor "$release_sha" origin/main | |
| if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then | |
| test "$GITHUB_REF" = "refs/heads/main" | |
| test "$RELEASE_TAG" = "v0.1.0" | |
| test "$tag_object" = "1bac90e875b7373fe391f7fbe359783ef545a522" | |
| test "$release_sha" = "82f510c3a40aa576bfeef277dd406d42b788250b" | |
| fi | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Verify release version | |
| run: | | |
| python - "$RELEASE_TAG" <<'PY' | |
| import json | |
| import re | |
| import sys | |
| import tomllib | |
| from pathlib import Path | |
| project = tomllib.loads(Path("pyproject.toml").read_text())["project"] | |
| version = project["version"] | |
| tag = sys.argv[1] | |
| tag_pattern = ( | |
| r"^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" | |
| r"(?:(?:a|b|rc)(?:0|[1-9]\d*))?$" | |
| ) | |
| if not re.fullmatch(tag_pattern, tag): | |
| raise SystemExit(f"tag {tag} is not a supported release tag") | |
| if project["name"] != "molarverse-pqviewer": | |
| raise SystemExit("unexpected Python distribution name") | |
| if tag != f"v{version}": | |
| raise SystemExit(f"tag {tag} does not match version {version}") | |
| frontend = json.loads(Path("frontend/package.json").read_text())["version"] | |
| lockfile = json.loads(Path("frontend/package-lock.json").read_text()) | |
| citation = Path("CITATION.cff").read_text() | |
| changelog = Path("CHANGELOG.md").read_text() | |
| if ( | |
| frontend != version | |
| or lockfile["version"] != version | |
| or lockfile["packages"][""]["version"] != version | |
| ): | |
| raise SystemExit("frontend version does not match the release") | |
| if not re.search(rf"^version: {re.escape(version)}$", citation, re.MULTILINE): | |
| raise SystemExit("CITATION.cff version does not match the release") | |
| citation_date = re.search( | |
| r'^date-released: ["\']?(\d{4}-\d{2}-\d{2})["\']?$', | |
| citation, | |
| re.MULTILINE, | |
| ) | |
| changelog_date = re.search( | |
| rf"^## \[{re.escape(version)}\] - (\d{{4}}-\d{{2}}-\d{{2}})$", | |
| changelog, | |
| re.MULTILINE, | |
| ) | |
| if citation_date is None or changelog_date is None: | |
| raise SystemExit("release date is missing from CITATION.cff or CHANGELOG.md") | |
| if citation_date.group(1) != changelog_date.group(1): | |
| raise SystemExit("release dates do not match") | |
| PY | |
| - name: Install Python dependencies | |
| run: python -m pip install --upgrade ".[dev]" build twine | |
| - name: Run Python tests | |
| run: python -m pytest | |
| - name: Build bundled interface | |
| working-directory: frontend | |
| run: | | |
| npm ci | |
| npm test | |
| npm run build | |
| - name: Verify bundled interface | |
| run: git diff --exit-code -- pqviewer/static | |
| - name: Build Python distributions | |
| run: | | |
| python -m build | |
| python -m twine check --strict dist/* | |
| wheel=$(find dist -name '*.whl' -print -quit) | |
| sdist=$(find dist -name '*.tar.gz' -print -quit) | |
| python scripts/verify_distribution.py "$wheel" "$sdist" | |
| - name: Upload Python distributions | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: python-distributions | |
| path: dist/ | |
| if-no-files-found: error | |
| retention-days: 14 | |
| smoke: | |
| needs: build | |
| strategy: | |
| matrix: | |
| os: | |
| - ubuntu-24.04 | |
| - macos-15 | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: python-distributions | |
| path: dist | |
| - name: Install wheel | |
| run: | | |
| wheel=$(find dist -name '*.whl' -print -quit) | |
| python -m pip install "$wheel" | |
| - name: Verify installed application | |
| run: | | |
| python -m pip check | |
| python - <<'PY' | |
| from importlib.resources import files | |
| import pqviewer | |
| assert (files("pqviewer") / "static" / "index.html").is_file() | |
| assert pqviewer.__name__ == "pqviewer" | |
| PY | |
| pqviewer --help | |
| pqviewer render --help | |
| pqviewer-render --help | |
| - name: Smoke-test local server | |
| run: | | |
| pqviewer --no-open --port 8765 > pqviewer.log 2>&1 & | |
| server_pid=$! | |
| trap 'kill "$server_pid" 2>/dev/null || true' EXIT | |
| for attempt in {1..30}; do | |
| if curl --fail --silent http://127.0.0.1:8765/api/health > /dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if ! curl --fail --silent http://127.0.0.1:8765/api/health; then | |
| cat pqviewer.log | |
| exit 1 | |
| fi | |
| curl --fail --silent http://127.0.0.1:8765/ > /dev/null | |
| kill "$server_pid" 2>/dev/null || true | |
| wait "$server_pid" || true | |
| trap - EXIT | |
| browser: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ inputs.tag && format('refs/tags/{0}', inputs.tag) || github.ref }} | |
| - name: Verify recovery source | |
| if: github.event_name == 'workflow_dispatch' | |
| run: test "$(git rev-parse HEAD)" = "82f510c3a40aa576bfeef277dd406d42b788250b" | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| - name: Install Python dependencies | |
| run: python -m pip install ".[dev,render]" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install browser dependencies | |
| working-directory: frontend | |
| run: | | |
| npm ci | |
| npx playwright install --with-deps chromium | |
| - name: Install render browser | |
| run: python -m playwright install chromium | |
| - name: Run browser tests | |
| working-directory: frontend | |
| run: npm run test:e2e | |
| - name: Smoke-test headless rendering | |
| env: | |
| PQVIEWER_HEADLESS_TEST: "1" | |
| run: python -m pytest tests/test_recipe.py -k headless_render_command | |
| - name: Upload browser artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-browser-artifacts | |
| path: output/playwright | |
| if-no-files-found: ignore | |
| publish-pypi: | |
| needs: | |
| - smoke | |
| - browser | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/molarverse-pqviewer | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: python-distributions | |
| path: dist | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 | |
| github-release: | |
| needs: | |
| - smoke | |
| - publish-pypi | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: python-distributions | |
| path: dist | |
| - name: Add release files | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| cd dist | |
| sha256sum *.whl *.tar.gz > SHA256SUMS | |
| release_flags=() | |
| if [[ "$RELEASE_TAG" =~ (a|b|rc)[0-9]+$ ]]; then | |
| release_flags+=(--prerelease) | |
| fi | |
| gh release create "$RELEASE_TAG" \ | |
| ./*.whl ./*.tar.gz ./SHA256SUMS \ | |
| --generate-notes \ | |
| --title "PQViewer ${RELEASE_TAG#v}" \ | |
| --verify-tag \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| "${release_flags[@]}" |