Skip to content

Release

Release #6

Workflow file for this run

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})`);