diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..eb81efa --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,47 @@ +# Releasing OpenYXDB + +Standard procedure for cutting an OpenYXDB release and publishing to PyPI. + +## Steps + +1. **Bump the version.** Update `version` in `pyproject.toml`. +2. **Update the changelog.** Move the `[Unreleased]` notes in + [CHANGELOG.md](CHANGELOG.md) under a new `## [X.Y.Z] - YYYY-MM-DD` + heading. +3. **Commit.** `release: vX.Y.Z` (see `git log` for the established style). + Push the commit to `main` (directly or via PR) and let CI run on it. +4. **Confirm CI is green before tagging.** `publish.yml` triggers directly + on `push: tags: ["v*"]` and has no way to depend on `ci.yml` passing - + GitHub Actions can't `needs:` a job defined in another workflow file. + So this has to be checked by hand, before the tag exists, not after: + + ```sh + scripts/check-release-ready.sh main + ``` + + This resolves the ref to a commit and checks that the most recent + `ci.yml` run for that commit completed successfully. It refuses (exit 1) + if CI hasn't run yet or didn't pass. Run it against the exact commit + you're about to tag. + +5. **Tag.** Once the check passes, create an annotated tag on that commit + and push it: + + ```sh + git tag -a vX.Y.Z -m "vX.Y.Z" + git push origin vX.Y.Z + ``` + + The tag push triggers `publish.yml`, which builds the sdist and wheels + (Linux, macOS, Windows) and publishes to PyPI via trusted publishing. +6. **Verify.** Watch the run (`gh run watch` or the Actions tab) and check + the new version shows up on [PyPI](https://pypi.org/project/openyxdb/). + +## Notes + +- Not every version bump has to be tagged/published immediately; a + version can be bumped and committed to `main` without a corresponding + tag if the release itself isn't ready to ship yet. Only a pushed `vX.Y.Z` + tag triggers a publish. +- This repo has no `audit.yml`, so `check-release-ready.sh` only checks + `ci.yml`. diff --git a/scripts/check-release-ready.sh b/scripts/check-release-ready.sh new file mode 100755 index 0000000..9e29b00 --- /dev/null +++ b/scripts/check-release-ready.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Refuse to say a commit is release-ready unless the most recent ci.yml run +# for that commit completed successfully. +# +# publish.yml triggers directly on `push: tags: ["v*"]` and GitHub Actions +# has no way for one workflow file to `needs:` a job defined in a separate +# workflow file, so this check has to run before the tag is created (see +# RELEASING.md and https://github.com/Sigilweaver/OpenYXDB/issues/1). +# +# This repo has no audit.yml, so only ci.yml is checked. +# +# Usage: scripts/check-release-ready.sh [ref] +# ref defaults to HEAD. + +set -euo pipefail + +ref="${1:-HEAD}" +# `^{commit}` peels annotated tags to the commit they point at; git rev-parse +# on a bare tag name otherwise returns the tag object's own SHA, which never +# has a CI run against it. +sha="$(git rev-parse "${ref}^{commit}")" + +echo "Checking release readiness for $ref ($sha)..." + +check_workflow() { + local workflow="$1" + local runs + runs="$(gh run list -w "$workflow" -c "$sha" --json status,conclusion,url -L 1)" + + if [[ "$(echo "$runs" | jq 'length')" -eq 0 ]]; then + echo "FAIL: no run of $workflow found for commit $sha" >&2 + return 1 + fi + + local status conclusion url + status="$(echo "$runs" | jq -r '.[0].status')" + conclusion="$(echo "$runs" | jq -r '.[0].conclusion')" + url="$(echo "$runs" | jq -r '.[0].url')" + + if [[ "$status" != "completed" ]]; then + echo "FAIL: latest $workflow run for $sha has not completed (status=$status) - $url" >&2 + return 1 + fi + + if [[ "$conclusion" != "success" ]]; then + echo "FAIL: latest $workflow run for $sha did not succeed (conclusion=$conclusion) - $url" >&2 + return 1 + fi + + echo "OK: $workflow succeeded for $sha - $url" + return 0 +} + +if check_workflow "ci.yml"; then + echo "Release ready: ci.yml is green for $sha" + exit 0 +fi + +exit 1