v1.4.1 #3
Workflow file for this run
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: PyPI Release | |
| # Triggered when a GitHub Release is published. | |
| # Publishes the full app package with the version derived from the release tag: | |
| # - deeptutor (Python backend/CLI + packaged Next.js standalone assets) | |
| # | |
| # The CLI-only package is intentionally not published to PyPI; install it from | |
| # a local checkout with `python -m pip install -e ./packaging/deeptutor-cli`. | |
| # | |
| # Expected tag format: vMAJOR.MINOR.PATCH, with PEP 440-compatible suffixes allowed. | |
| # Example: GitHub release tag v1.3.10 -> PyPI version 1.3.10. | |
| # | |
| # PyPI authentication uses Trusted Publishing. Configure the `deeptutor` PyPI | |
| # project to trust this workflow file, then no API token secret is needed. | |
| on: | |
| release: | |
| types: [published] | |
| permissions: | |
| contents: read | |
| id-token: write | |
| concurrency: | |
| group: pypi-release-${{ github.event.release.tag_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-and-publish: | |
| name: Build and Publish PyPI Package | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/deeptutor/ | |
| steps: | |
| - name: Checkout release tag | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.release.tag_name }} | |
| fetch-depth: 0 | |
| - name: Verify release tag is on main | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch origin main:refs/remotes/origin/main --no-tags | |
| tag="${{ github.event.release.tag_name }}" | |
| tag_commit="$(git rev-list -n 1 "$tag")" | |
| if ! git merge-base --is-ancestor "$tag_commit" origin/main; then | |
| echo "Release tag $tag ($tag_commit) is not on origin/main; refusing to publish to PyPI." >&2 | |
| exit 1 | |
| fi | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Install build tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install --upgrade build packaging twine | |
| - name: Verify release tag matches deeptutor/__version__.py | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import os | |
| import re | |
| from pathlib import Path | |
| from packaging.version import Version | |
| tag = os.environ["RELEASE_TAG"] | |
| tag_raw = tag[1:] if tag.startswith("v") else tag | |
| tag_version = str(Version(tag_raw)) | |
| src = Path("deeptutor/__version__.py").read_text(encoding="utf-8") | |
| match = re.search(r'^__version__\s*=\s*"([^"]+)"', src, re.MULTILINE) | |
| if not match: | |
| raise SystemExit( | |
| "Could not find __version__ in deeptutor/__version__.py" | |
| ) | |
| code_version = str(Version(match.group(1))) | |
| if code_version != tag_version: | |
| raise SystemExit( | |
| f"Release tag {tag!r} (normalized {tag_version!r}) does not " | |
| f"match deeptutor/__version__.py value {code_version!r}. " | |
| "Bump __version__ before tagging." | |
| ) | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle: | |
| handle.write(f"version={code_version}\n") | |
| PY | |
| env: | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| - name: Install frontend dependencies | |
| working-directory: web | |
| run: npm ci --legacy-peer-deps | |
| - name: Build packaged Web assets | |
| run: python scripts/prepare_web_package.py | |
| env: | |
| NEXT_PUBLIC_API_BASE: __NEXT_PUBLIC_API_BASE_PLACEHOLDER__ | |
| NEXT_PUBLIC_AUTH_ENABLED: __NEXT_PUBLIC_AUTH_ENABLED_PLACEHOLDER__ | |
| - name: Build distributions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist/deeptutor | |
| # Run from /tmp so any repository-local build/ directory cannot shadow | |
| # PyPA's `build` module during repeated package builds. | |
| # Wheels are the supported public install artifacts. The full app | |
| # wheel contains packaged Next.js assets; source distributions would | |
| # either duplicate those large assets or require a frontend build. | |
| (cd /tmp && python -m build --wheel --outdir "$GITHUB_WORKSPACE/dist/deeptutor" "$GITHUB_WORKSPACE") | |
| - name: Verify package metadata | |
| run: | | |
| python -m twine check dist/deeptutor/* | |
| python - <<'PY' | |
| from pathlib import Path | |
| expected = "${{ steps.version.outputs.version }}" | |
| files = { | |
| "deeptutor": sorted(path.name for path in Path("dist/deeptutor").glob("*")), | |
| } | |
| for package, names in files.items(): | |
| print(package) | |
| print("\n".join(f" {name}" for name in names)) | |
| required = [ | |
| ("deeptutor", f"deeptutor-{expected}-py3-none-any.whl"), | |
| ] | |
| missing = [name for package, name in required if name not in files[package]] | |
| if missing: | |
| raise SystemExit(f"Missing expected distributions: {missing}") | |
| PY | |
| - name: Publish deeptutor to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist/deeptutor/ | |
| print-hash: true |