release #65
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: | |
| # Manual tag push -> create release (fallback / first-time bootstrap). | |
| push: | |
| tags: ["v*"] | |
| # CI passed on main -> auto-bump if bumpable commits exist. | |
| workflow_run: | |
| workflows: ["ci"] | |
| branches: [main] | |
| types: [completed] | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Auto-bump after CI passes on main. | |
| # commitizen inspects conventional commits since the last tag: | |
| # feat -> minor, fix -> patch, BREAKING CHANGE -> major. | |
| # docs / refactor / chore / etc. -> no bump, job exits cleanly. | |
| # | |
| # The bump commit + tag are pushed with GITHUB_TOKEN, which does NOT | |
| # re-trigger workflows -- no infinite loop. | |
| bump-and-release: | |
| if: >- | |
| github.event_name == 'workflow_run' | |
| && github.event.workflow_run.conclusion == 'success' | |
| && github.event.workflow_run.event == 'push' | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - run: pip install commitizen | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Bump version | |
| id: bump | |
| run: | | |
| set -o pipefail | |
| # commitizen exit codes worth distinguishing: | |
| # 0 = bumped successfully | |
| # 3 = NO_COMMITS_FOUND (nothing bumpable since last tag, exit clean) | |
| # 21 = NO_COMMITS_FOUND when no commits found between tags | |
| # Anything else (consistency failure, IO failure, ...) MUST fail the | |
| # job loudly. The previous shape `if cz bump ...; then ... else ...` | |
| # silently swallowed every non-zero exit and quietly skipped the | |
| # release; consistency drift between version_files and the source | |
| # tree went unnoticed for multiple commits. | |
| set +e | |
| cz bump --yes --check-consistency 2>&1 | tee /tmp/cz.log | |
| status=${PIPESTATUS[0]} | |
| set -e | |
| case "$status" in | |
| 0) | |
| echo "bumped=true" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$(cz version --project)" >> "$GITHUB_OUTPUT" | |
| ;; | |
| 3|21) | |
| echo "no bumpable commits since last tag; exiting clean" | |
| echo "bumped=false" >> "$GITHUB_OUTPUT" | |
| ;; | |
| *) | |
| echo "::error::cz bump failed with status $status (see log above)" | |
| exit "$status" | |
| ;; | |
| esac | |
| - name: Regenerate amalgamation | |
| if: steps.bump.outputs.bumped == 'true' | |
| run: python3 tools/amalgamate.py | |
| - name: Sync uv lockfile | |
| if: steps.bump.outputs.bumped == 'true' | |
| run: pipx run uv lock | |
| - name: Commit post-bump artifacts if changed | |
| if: steps.bump.outputs.bumped == 'true' | |
| run: | | |
| if ! git diff --quiet -- single_include/citor.hpp uv.lock; then | |
| git add single_include/citor.hpp uv.lock | |
| git commit --amend --no-edit | |
| git tag -f "${{ steps.bump.outputs.tag }}" | |
| fi | |
| - name: Push | |
| if: steps.bump.outputs.bumped == 'true' | |
| run: | | |
| git push --force-with-lease origin main | |
| git push --force-with-lease origin "${{ steps.bump.outputs.tag }}" | |
| - name: Stage release artifacts | |
| if: steps.bump.outputs.bumped == 'true' | |
| run: | | |
| tag="${{ steps.bump.outputs.tag }}" | |
| mkdir -p release-artifacts | |
| cp single_include/citor.hpp "release-artifacts/citor-${tag}.hpp" | |
| ( cd release-artifacts && sha256sum "citor-${tag}.hpp" > "citor-${tag}.hpp.sha256" ) | |
| ( cd single_include && tar czf "../release-artifacts/citor-${tag}-single-header.tar.gz" citor.hpp ) | |
| ( cd release-artifacts && sha256sum "citor-${tag}-single-header.tar.gz" > "citor-${tag}-single-header.tar.gz.sha256" ) | |
| - name: Create or refresh GitHub release | |
| if: steps.bump.outputs.bumped == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ steps.bump.outputs.tag }}" | |
| PREV=$(git tag -l 'v*' --sort=-version:refname | sed -n '2p') | |
| { | |
| echo "## What's Changed" | |
| echo "" | |
| if [ -n "$PREV" ]; then | |
| git log "${PREV}..${TAG}^" --pretty=format:'- %s' --no-merges | |
| else | |
| git log "${TAG}^" --pretty=format:'- %s' --no-merges --max-count=50 | |
| fi | |
| echo "" | |
| if [ -n "$PREV" ]; then | |
| echo "**Full diff**: https://github.com/${{ github.repository }}/compare/${PREV}...${TAG}" | |
| fi | |
| } > /tmp/notes.md | |
| # Idempotent. The bump-and-release path can re-fire on a force-push | |
| # that lands the same tag at a new SHA; the release-on-tag path can | |
| # race with this job. When the release already exists, refresh | |
| # notes and asset payloads in place; otherwise create. | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "release $TAG exists; refreshing notes and assets" | |
| gh release edit "$TAG" --notes-file /tmp/notes.md | |
| gh release upload "$TAG" release-artifacts/* --clobber | |
| else | |
| echo "release $TAG missing; creating" | |
| gh release create "$TAG" --title "$TAG" --notes-file /tmp/notes.md release-artifacts/* | |
| fi | |
| # Manual tag release: local `cz bump` + `git push --follow-tags`, | |
| # or any hand-pushed v* tag. Tags pushed by bump-and-release use | |
| # GITHUB_TOKEN so they do NOT trigger this job (no double release). | |
| release-on-tag: | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Regenerate amalgamation | |
| run: python3 tools/amalgamate.py | |
| - name: Stage release artifacts | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| mkdir -p release-artifacts | |
| cp single_include/citor.hpp "release-artifacts/citor-${tag}.hpp" | |
| ( cd release-artifacts && sha256sum "citor-${tag}.hpp" > "citor-${tag}.hpp.sha256" ) | |
| ( cd single_include && tar czf "../release-artifacts/citor-${tag}-single-header.tar.gz" citor.hpp ) | |
| ( cd release-artifacts && sha256sum "citor-${tag}-single-header.tar.gz" > "citor-${tag}-single-header.tar.gz.sha256" ) | |
| - name: Create or refresh GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ github.ref_name }}" | |
| PREV=$(git tag -l 'v*' --sort=-version:refname | sed -n '2p') | |
| { | |
| echo "## What's Changed" | |
| echo "" | |
| if [ -n "$PREV" ]; then | |
| git log "${PREV}..${TAG}" --pretty=format:'- %s' --no-merges | |
| else | |
| git log "${TAG}" --pretty=format:'- %s' --no-merges --max-count=50 | |
| fi | |
| echo "" | |
| if [ -n "$PREV" ]; then | |
| echo "**Full diff**: https://github.com/${{ github.repository }}/compare/${PREV}...${TAG}" | |
| fi | |
| } > /tmp/notes.md | |
| # Idempotent. `release-on-tag` fires on every `v*` tag push, which | |
| # includes force-moves (tag rewrites during a history-rewrite | |
| # session). When the release exists, refresh notes and assets in | |
| # place; otherwise create it. | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "release $TAG exists; refreshing notes and assets" | |
| gh release edit "$TAG" --notes-file /tmp/notes.md | |
| gh release upload "$TAG" release-artifacts/* --clobber | |
| else | |
| echo "release $TAG missing; creating" | |
| gh release create "$TAG" --title "$TAG" --notes-file /tmp/notes.md release-artifacts/* | |
| fi |