Skip to content

ci: version bump

ci: version bump #3

Workflow file for this run

name: Publish to PyPI on version change
on:
push:
branches: [ main ]
permissions:
contents: write
id-token: write
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Check out repo (full history for diff & tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python 3.11 (for tomllib)
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: pyproject.toml
- name: Determine if version changed in pyproject.toml
id: ver
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
python - << 'PY'
import io, os, subprocess, sys, re, textwrap, datetime
try:
import tomllib # py311+
except Exception as e:
print("tomllib missing", file=sys.stderr); raise
def load_version_from_bytes(b: bytes):
return tomllib.load(io.BytesIO(b))["project"]["version"]
# Current version (from workspace)
with open("pyproject.toml","rb") as f:
current = load_version_from_bytes(f.read())
# Previous version (from commit before this push)
before = os.environ.get("BEFORE_SHA") or ""
prev = None
if before and before != "0000000000000000000000000000000000000000":
try:
blob = subprocess.check_output(["git","show",f"{before}:pyproject.toml"])
prev = load_version_from_bytes(blob)
except subprocess.CalledProcessError:
prev = None
changed = (prev is None) or (current != prev)
# Stash values for later steps
with open(os.environ["GITHUB_OUTPUT"],"a") as out:
out.write(f"current={current}\n")
out.write(f"previous={prev or ''}\n")
out.write(f"changed={'true' if changed else 'false'}\n")
print(f"Current version: {current}\nPrevious version: {prev}\nChanged: {changed}")
PY
# ---------- Build & Publish to PyPI (only if version changed) ----------
- name: Build sdist + wheel
if: steps.ver.outputs.changed == 'true'
run: |
python -m pip install -U pip
pip install build
python -m build
- name: Publish to PyPI (Trusted Publishing)
if: steps.ver.outputs.changed == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
# ---------- Prepare CHANGELOG update & release notes ----------
- name: Prepare CHANGELOG update & release notes
id: cl
if: steps.ver.outputs.changed == 'true'
env:
VERSION: ${{ steps.ver.outputs.current }}
PREV_VERSION: ${{ steps.ver.outputs.previous }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
python - << 'PY'
import os, re, sys, datetime, pathlib
repo = os.environ["REPO"] # e.g., org/repo
version = os.environ["VERSION"] # e.g., 0.1.2
prev = os.environ.get("PREV_VERSION") or "" # e.g., 0.1.1 or ""
changelog_path = pathlib.Path("CHANGELOG.md")
if not changelog_path.exists():
# Some repos use Changelog.md
alt = pathlib.Path("Changelog.md")
if alt.exists():
changelog_path = alt
else:
print("No CHANGELOG.md found", file=sys.stderr)
sys.exit(1)
text = changelog_path.read_text(encoding="utf-8")
# Grab the [Unreleased] block (non-greedy until the next version header or EOF)
unreleased_re = re.compile(r"^## \\[Unreleased\\]\\s*(.*?)\\s*(?=^## \\[|\\Z)", re.S | re.M)
m = unreleased_re.search(text)
unreleased_body = (m.group(1).strip() if m else "").strip()
if not unreleased_body:
unreleased_body = "_No notable changes._"
# Build the new version section
today = datetime.date.today().isoformat() # YYYY-MM-DD
new_section = f"## [{version}] - {today}\\n{unreleased_body}\\n\\n"
# Replace the Unreleased section with an empty placeholder
if m:
start, end = m.span()
prefix = text[:m.start()]
suffix = text[m.end():]
# Minimal placeholder keeps it tidy
unreleased_placeholder = "## [Unreleased]\\n\\n"
text = prefix + unreleased_placeholder + suffix
else:
# If there was no [Unreleased], put one at the top under the main title
text = text.replace("## Changelog", "## Changelog\\n\\n## [Unreleased]\\n")
# Insert the new version section right after [Unreleased]
insert_point = text.find("## [Unreleased]")
if insert_point != -1:
# Find end of that line to insert after
insert_point = text.find("\n", insert_point)
if insert_point == -1:
insert_point = len(text)
text = text[:insert_point+1] + new_section + text[insert_point+1:]
else:
# Fallback: prepend
text = new_section + text
# ----- Update link reference section at the bottom -----
# Ensure we have lines for [Unreleased] and the new version comparing prev..current
# Remove any existing [Unreleased]: line to replace cleanly
lines = text.rstrip() .splitlines()
# Find if there's an existing reference block (last contiguous lines that look like "[x]: url")
# We'll just rebuild/append ours safely.
def set_ref(name, url):
ref = f"[{name}]: {url}"
# replace existing or append
for i, ln in enumerate(lines):
if ln.startswith(f"[{name}]"):
lines[i] = ref
return
lines.append(ref)
base = f"https://github.com/{repo}"
# previous version for compare link; if unknown, compare from first commit
prev_tag = f"v{prev}" if prev else "v0.0.0"
set_ref("Unreleased", f"{base}/compare/v{version}...HEAD")
set_ref(version, f"{base}/compare/{prev_tag}...v{version}")
new_text = "\n".join(lines) + "\n"
# Write a temp file for release notes (just the body we moved)
pathlib.Path("RELEASE_NOTES.md").write_text(unreleased_body + "\n", encoding="utf-8")
# And write the updated changelog to a temp file (we only commit after publish)
pathlib.Path("CHANGELOG.updated.md").write_text(new_text, encoding="utf-8")
PY
# ---------- Commit CHANGELOG, tag, and create GitHub Release ----------
- name: Commit updated CHANGELOG
if: steps.ver.outputs.changed == 'true'
run: |
mv CHANGELOG.updated.md CHANGELOG.md
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "chore(release): v${{ steps.ver.outputs.current }} [skip ci]"
git push
- name: Create and push tag
if: steps.ver.outputs.changed == 'true'
run: |
git tag -a "v${{ steps.ver.outputs.current }}" -m "v${{ steps.ver.outputs.current }}"
git push origin "v${{ steps.ver.outputs.current }}"
- name: Create GitHub Release with notes from CHANGELOG
if: steps.ver.outputs.changed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ steps.ver.outputs.current }}" \
--title "v${{ steps.ver.outputs.current }}" \
--notes-file RELEASE_NOTES.md