Release #6
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 | |
| # Triggered by CI completing on main. We do NOT trigger on push directly | |
| # so a release can never ship if the test matrix failed. | |
| # | |
| # Auto-bump model: every CI-success push to main increments the patch | |
| # version (0.1.0 → 0.1.1 → 0.1.2 → …). The bump itself lands as a | |
| # `chore: bump version to X.Y.Z` commit pushed back to main; that push | |
| # uses GITHUB_TOKEN, which by GitHub's rules does NOT trigger CI again, | |
| # so there's no infinite loop. As belt-and-suspenders we also skip the | |
| # bump if the previous commit was already an auto-bump. | |
| # | |
| # Escape hatch: a commit message containing `[skip release]` opts out | |
| # of bumping AND publishing for that push. | |
| # | |
| # CI (ci.yml) succeeds on main push | |
| # │ | |
| # ▼ workflow_run: completed + conclusion == success | |
| # bump-version → publish-pypi → create-draft-release | |
| # │ | |
| # ▼ | |
| # build-nuitka [linux | windows | macos] | |
| # │ | |
| # ▼ | |
| # publish-release (unmark draft) | |
| on: | |
| workflow_run: | |
| workflows: [CI] | |
| types: [completed] | |
| branches: [main] | |
| permissions: | |
| # contents:write is needed to push the bump commit, create tags + | |
| # releases, and upload release assets (the Nuitka binaries). | |
| contents: write | |
| concurrency: | |
| group: release-main | |
| cancel-in-progress: false | |
| jobs: | |
| bump-version: | |
| name: Auto-bump patch version | |
| # Only run when CI succeeded. workflow_run fires on every CI | |
| # completion (success, failure, cancelled), so we have to gate | |
| # this explicitly. | |
| if: github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.bump.outputs.version }} | |
| sha: ${{ steps.push.outputs.sha }} | |
| skipped: ${{ steps.gate.outputs.skip }} | |
| steps: | |
| - name: Checkout main | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| # Need history to inspect the previous commit message for | |
| # loop detection. | |
| fetch-depth: 2 | |
| # persist-credentials lets the final `git push` reuse the | |
| # GITHUB_TOKEN this job was issued. | |
| persist-credentials: true | |
| - name: Decide whether to bump | |
| id: gate | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| MSG=$(git log -1 --pretty=%B) | |
| if echo "$MSG" | grep -q '\[skip release\]'; then | |
| echo "Commit carries [skip release]; not bumping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| elif echo "$MSG" | grep -q '^chore: bump version to '; then | |
| # The previous push WAS an auto-bump. GitHub's rule about | |
| # GITHUB_TOKEN-pushes not triggering workflows should have | |
| # already broken any loop, but this is a safety net. | |
| echo "Previous commit was an auto-bump; not re-bumping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Configure git author | |
| if: steps.gate.outputs.skip == 'false' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Bump patch version | |
| id: bump | |
| if: steps.gate.outputs.skip == 'false' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| OLD=$(grep -E '^version *= *"' pyproject.toml | head -1 \ | |
| | sed -E 's/.*"([^"]+)".*/\1/') | |
| if [ -z "$OLD" ]; then | |
| echo "::error::could not read current version from pyproject.toml" | |
| exit 1 | |
| fi | |
| # Split X.Y.Z and increment Z. If the version has more or | |
| # fewer components, fail loudly rather than guess. | |
| if ! [[ "$OLD" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then | |
| echo "::error::version $OLD is not a plain X.Y.Z — pre-release tags require manual bump" | |
| exit 1 | |
| fi | |
| X="${BASH_REMATCH[1]}" | |
| Y="${BASH_REMATCH[2]}" | |
| Z="${BASH_REMATCH[3]}" | |
| NEW="$X.$Y.$((Z+1))" | |
| sed -i "s|^version = \"$OLD\"|version = \"$NEW\"|" pyproject.toml | |
| echo "Bumped $OLD → $NEW" | |
| echo "version=$NEW" >> "$GITHUB_OUTPUT" | |
| - name: Commit + push bump | |
| id: push | |
| if: steps.gate.outputs.skip == 'false' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git add pyproject.toml | |
| git commit -m "chore: bump version to ${{ steps.bump.outputs.version }}" | |
| git push origin HEAD:main | |
| # Capture the SHA of the bump commit so downstream jobs build | |
| # exactly that revision. | |
| echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| publish-pypi: | |
| name: Build + publish to PyPI | |
| needs: bump-version | |
| if: needs.bump-version.outputs.skipped == 'false' | |
| runs-on: ubuntu-latest | |
| # No `environment:` block on purpose. Attaching one would make | |
| # GitHub categorise the publish as a Deployment and surface it | |
| # under the repo's "Deployments" sidebar widget. The artefact | |
| # of a successful run is a GitHub Release + a PyPI version — | |
| # both already have first-class UI in their respective places — | |
| # so the Deployment view is redundant noise on the repo home. | |
| steps: | |
| - name: Checkout the bumped commit | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.bump-version.outputs.sha }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install build backend | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build sdist + wheel | |
| run: python -m build | |
| - name: Verify distribution metadata | |
| run: python -m twine check dist/* | |
| - name: Publish to PyPI | |
| env: | |
| TWINE_USERNAME: __token__ | |
| TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} | |
| run: python -m twine upload --non-interactive dist/* | |
| create-draft-release: | |
| name: Create draft GitHub release | |
| needs: [bump-version, publish-pypi] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout the bumped commit | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.bump-version.outputs.sha }} | |
| - name: Create draft release | |
| # Draft now, attach Nuitka assets in subsequent jobs, unmark | |
| # draft once everything's uploaded — so consumers never see a | |
| # half-finished release. | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.bump-version.outputs.version }} | |
| name: v${{ needs.bump-version.outputs.version }} | |
| draft: true | |
| generate_release_notes: true | |
| target_commitish: ${{ needs.bump-version.outputs.sha }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| build-nuitka: | |
| name: Nuitka build (windows-x86_64) | |
| needs: [bump-version, create-draft-release] | |
| # Windows is the only platform users have asked for an .exe on. | |
| # Linux/macOS users install from PyPI; shipping a Nuitka binary | |
| # there just inflates the release page without serving a real | |
| # use case. | |
| runs-on: windows-latest | |
| # PySide6 cold builds run ~50-70 min on a fresh runner (Qt is | |
| # a huge amount of C++ to link). With cache they're back to 5-10 | |
| # min. The cap below covers cold + slowest-case parallel link. | |
| timeout-minutes: 90 | |
| env: | |
| ASSET_NAME: autopapertoppt-windows-x86_64.exe | |
| steps: | |
| - name: Checkout the bumped commit | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.bump-version.outputs.sha }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Cache Nuitka build artefacts | |
| uses: actions/cache@v4 | |
| with: | |
| # ~/.nuitka holds Nuitka's own caches; *.build holds the | |
| # generated C sources from the previous run. Caching both | |
| # cuts subsequent builds from ~15 min to ~3 min. | |
| path: | | |
| ~/.nuitka | |
| ~/.cache/Nuitka | |
| autopapertoppt.build | |
| autopapertoppt.dist | |
| key: nuitka-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}-${{ needs.bump-version.outputs.version }} | |
| restore-keys: | | |
| nuitka-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}- | |
| nuitka-${{ runner.os }}- | |
| - name: Install runtime + Nuitka | |
| # Install the intelligence + mcp + gui extras so the shipped | |
| # exe supports --enrich, the MCP entry point, AND the PySide6 | |
| # desktop UI (`autopapertoppt.exe gui`). nuitka is only needed | |
| # for the build. | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[intelligence,mcp,gui]" | |
| pip install nuitka | |
| - name: Compile with Nuitka | |
| # All 11 source plugins are force-included because they're | |
| # imported dynamically by name at runtime (see | |
| # autopapertoppt/fetchers/base.py::load_fetcher). The plugins | |
| # live under sources/<name>/ and are NOT installed as Python | |
| # packages, so we prepend sources/ to PYTHONPATH to make them | |
| # importable during the build. At runtime the app does the | |
| # equivalent sys.path injection itself. | |
| # | |
| # The sources/ directory ALSO ships as data so the runtime can | |
| # read the unmodified .py files for its own sys.path lookup. | |
| # | |
| # python-pptx imports as `pptx` (PyPI name -> module name | |
| # mismatch is normal); use the module name here. | |
| # | |
| # PySide6 is handled ENTIRELY by --enable-plugin=pyside6 — | |
| # adding --include-package=PySide6 / --include-package-data= | |
| # PySide6 on top of the plugin causes redundant inclusions | |
| # and roughly doubles the cold build time. The plugin itself | |
| # includes the full QML / translations / resources tree by | |
| # default, which is what we want — Qt features the GUI does | |
| # not use today may still be hit by future tabs (Deck inspect, | |
| # rich-text preview, etc.), and the size hit on a single | |
| # Windows .exe is acceptable. | |
| shell: bash | |
| env: | |
| PYTHONPATH: sources | |
| run: | | |
| python -m nuitka \ | |
| --standalone \ | |
| --onefile \ | |
| --output-filename=autopapertoppt.exe \ | |
| --include-package=autopapertoppt \ | |
| --include-package=arxiv \ | |
| --include-package=semantic_scholar \ | |
| --include-package=openalex \ | |
| --include-package=pubmed \ | |
| --include-package=acm \ | |
| --include-package=ieee \ | |
| --include-package=scholar \ | |
| --include-package=dblp \ | |
| --include-package=crossref \ | |
| --include-package=openaire \ | |
| --include-package=springer \ | |
| --enable-plugin=pyside6 \ | |
| --include-data-dir=./sources=sources \ | |
| --include-package-data=pptx \ | |
| --include-package-data=openpyxl \ | |
| --assume-yes-for-downloads \ | |
| autopapertoppt/__main__.py | |
| - name: Smoke-test the built executable | |
| # If the binary fails to load any of its bundled plugins, this | |
| # surfaces it immediately rather than at the user's first run. | |
| shell: bash | |
| run: | | |
| ./autopapertoppt.exe --version || true | |
| ./autopapertoppt.exe --help > /dev/null | |
| - name: Rename for the platform-specific asset name | |
| shell: bash | |
| run: mv autopapertoppt.exe "$ASSET_NAME" | |
| - name: Compute SHA-256 checksum | |
| # Attach the checksum file alongside the binary so users can | |
| # verify what they downloaded matches what CI built. | |
| shell: bash | |
| run: sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256" | |
| - name: Attach binary + checksum to release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.bump-version.outputs.version }} | |
| files: | | |
| ${{ env.ASSET_NAME }} | |
| ${{ env.ASSET_NAME }}.sha256 | |
| # The release was created as a draft above; uploading does | |
| # not unmark it. | |
| draft: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-release: | |
| name: Mark release as published | |
| needs: [bump-version, build-nuitka] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Unmark draft (publish the release) | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const tag = "v${{ needs.bump-version.outputs.version }}"; | |
| const { owner, repo } = context.repo; | |
| const release = await github.rest.repos.getReleaseByTag({ | |
| owner, repo, tag, | |
| }); | |
| await github.rest.repos.updateRelease({ | |
| owner, repo, | |
| release_id: release.data.id, | |
| draft: false, | |
| }); | |
| console.log(`Released ${tag} (${release.data.html_url})`); |