diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index f12ed42..3242af3 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -3,7 +3,7 @@ name: Build and publish to TestPyPI on: push: tags: - - "v*" + - "v*-rc*" workflow_dispatch: permissions: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fbde6ab..cf737ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,5 +76,5 @@ repos: entry: uv run --locked --group dev pytest scripts/test_release_lifecycle.py -v language: system pass_filenames: false - files: (\.bumpversion\.toml|scripts/(prepare-release|commit-release)\.sh|scripts/revert_changelog_rc\.py|scripts/test_release_lifecycle\.py|CHANGELOG\.md) + files: (\.bumpversion\.toml|scripts/(merge-bump|publish-release)\.sh|scripts/revert_changelog_rc\.py|scripts/test_release_lifecycle\.py|CHANGELOG\.md) stages: [pre-push] diff --git a/docs/releasing.md b/docs/releasing.md index 4d3e8af..30fbcb1 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -17,7 +17,7 @@ All publishing uses [PyPI Trusted Publishers (OIDC)](https://docs.pypi.org/trust - For RC releases: approval rights on the `pypi-publish-test` GitHub environment - For production releases: approval rights on the `pypi-publish-prod` GitHub environment - Local dev environment set up (see [CONTRIBUTING.md](../CONTRIBUTING.md)) -- On up-to-date main branch without local changes. +- On the default branch, in sync with remote (enforced by `merge-bump.sh`). ## Version format @@ -27,8 +27,8 @@ Version bumping is managed by [bump-my-version](https://github.com/callowayproje Two helper scripts handle the git workflow after bumping: -- `scripts/prepare-release.sh` — creates branch, syncs lockfile, commits, pushes, opens PR, watches CI -- `scripts/commit-release.sh` — checks out main, tags the release, pushes tag to trigger deployment +- `scripts/merge-bump.sh` — creates branch, syncs lockfile, commits, pushes, opens PR, watches CI, and squash-merges +- `scripts/publish-release.sh` — checks out main, tags the release, pushes tag to trigger deployment ## Preparation @@ -41,7 +41,7 @@ To avoid having to prepend all commands with `uv run`, simply activate the curre Ensure that your local `.venv` is in sync. ```bash -uv sync --locked --all-groups +uv sync --locked ``` ## Release candidate @@ -50,23 +50,16 @@ Use this to test a release on TestPyPI before publishing to production. ### 1. Bump version -From the up-to-date `main` branch, bump to the desired RC version: +From the up-to-date `main` branch, bump to the desired RC version (`0.2.1` → `0.3.0-rc0`): ```bash -bump-my-version bump minor # 0.1.0 → 0.2.0-rc0 +bump-my-version bump minor ``` -For subsequent release candidates: +For subsequent release candidates (`rc0` → `rc1`, `rc1` → `rc2`, etc.): ```bash -bump-my-version bump pre_n # rc0 → rc1, rc1 → rc2, etc. -``` - -To jump to a specific version (e.g. new minor): - -```bash -bump-my-version bump minor # 0.1.0 → 0.2.0-rc0 -bump-my-version bump pre_n # 0.2.0-rc0 → 0.2.0-rc1 +bump-my-version bump pre_n ``` Verify the result: @@ -78,22 +71,16 @@ bump-my-version show current_version ### 2. Prepare and merge ```bash -./scripts/prepare-release.sh +./scripts/merge-bump.sh ``` -This creates a branch, syncs the lockfile, commits, pushes, opens a PR, and watches CI. -For RC versions, it also removes the RC heading from `CHANGELOG.md` (keeping only `## Unreleased`). - -Once CI passes, merge: - -```bash -gh pr merge --merge -``` +This creates a branch, syncs the lockfile, commits, pushes, opens a PR, watches CI +and squash-merges it. For RC versions, it removes the RC heading from `CHANGELOG.md` (keeping only `## Unreleased`). ### 3. Tag and push ```bash -./scripts/commit-release.sh +./scripts/publish-release.sh ``` This checks out `main`, tags `v`, and pushes. Triggers `deploy-test.yml`: **build → publish to TestPyPI**. @@ -109,10 +96,10 @@ Check the package page at `https://test.pypi.org/project/gitfluence//` - The package is listed - Attestations are present (visible under "Provenance") -To test installation: +To test installation (uses `--index-strategy unsafe-best-match` so dependencies resolve from PyPI while pulling gitfluence from TestPyPI): ```bash -uv pip install -i https://test.pypi.org/simple/ gitfluence== +uv pip install --extra-index-url https://test.pypi.org/simple/ --index-strategy unsafe-best-match gitfluence== ``` ### 6. Iterate if needed @@ -123,10 +110,10 @@ Repeat steps 1–5 using `bump-my-version bump pre_n` to increment the RC number ### 1. Bump version -From the up-to-date `main` branch, bump to the final version: +From the up-to-date `main` branch, bump to the final version (e.g. `0.3.0-rc1` → `0.3.0`): ```bash -bump-my-version bump pre_l # e.g. 0.2.0-rc1 → 0.2.0 +bump-my-version bump pre_l ``` Verify: @@ -138,19 +125,13 @@ bump-my-version show current_version ### 2. Prepare and merge ```bash -./scripts/prepare-release.sh -``` - -Once CI passes, merge: - -```bash -gh pr merge --merge +./scripts/merge-bump.sh ``` ### 3. Tag and push ```bash -./scripts/commit-release.sh +./scripts/publish-release.sh ``` This triggers `deploy-prod.yml`: **build → GitHub Release → PyPI**. @@ -176,10 +157,10 @@ uv pip install gitfluence== ## Workflows -| Workflow | Trigger | Pipeline | Environment | -| ----------------- | ------------------------------------ | ---------------------- | ------------------- | -| `deploy-test.yml` | Any `v*` tag | build → TestPyPI | `pypi-publish-test` | -| `deploy-prod.yml` | `v${NEW_VERSION_FINAL}` tags (no rc) | build → release → PyPI | `pypi-publish-prod` | +| Workflow | Trigger | Pipeline | Environment | +| ----------------- | ------------------------------- | ---------------------- | ------------------- | +| `deploy-test.yml` | `v*-rc*` tags (RC only) | build → TestPyPI | `pypi-publish-test` | +| `deploy-prod.yml` | `v[0-9]+.[0-9]+.[0-9]+` (no rc) | build → release → PyPI | `pypi-publish-prod` | ## Security diff --git a/scripts/merge-bump.sh b/scripts/merge-bump.sh new file mode 100755 index 0000000..6c8b167 --- /dev/null +++ b/scripts/merge-bump.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Create a release branch from the current (already bumped) version, +# sync lockfile, commit, push, open PR, watch CI checks, and squash-merge. +# +# Usage: ./scripts/merge-bump.sh +# +# Prerequisites: +# - bump-my-version bump has already been run (without --commit) +# - Working directory is the repo root +# - gh CLI is authenticated + +set -euo pipefail + +trap 'echo "ERROR: Command failed at line ${LINENO}: ${BASH_COMMAND}" >&2' ERR + +# Ensure we're on the default branch and in sync with remote +DEFAULT_BRANCH="$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" +CURRENT_BRANCH="$(git branch --show-current)" +if [[ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then + echo "ERROR: Must be on '${DEFAULT_BRANCH}' branch (currently on '${CURRENT_BRANCH}'). Abort." >&2 + exit 1 +fi + +git fetch origin "$DEFAULT_BRANCH" +LOCAL_SHA="$(git rev-parse HEAD)" +REMOTE_SHA="$(git rev-parse "origin/${DEFAULT_BRANCH}")" +if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then + echo "ERROR: Local ${DEFAULT_BRANCH} (${LOCAL_SHA:0:8}) differs from remote (${REMOTE_SHA:0:8}). Pull or push first. Abort." >&2 + exit 1 +fi + +VERSION="$(bump-my-version show current_version)" +if [[ -z "$VERSION" ]]; then + echo "ERROR: Could not determine current version" >&2 + exit 1 +fi + +# Detect if this is an RC version +if [[ "$VERSION" =~ -rc[0-9]+$ ]]; then + BRANCH="chore/bump-${VERSION}" + PR_TITLE="Bumping version to ${VERSION}" + # Revert RC heading in CHANGELOG.md — keep only ## Unreleased + if ! python scripts/revert_changelog_rc.py; then + echo "WARNING: revert_changelog_rc.py failed" >&2 + fi +else + BRANCH="chore/release-${VERSION}" + PR_TITLE="Release ${VERSION}" +fi + +echo "==> Preparing release for v${VERSION} on branch ${BRANCH}" + +# Create branch +git checkout -b "${BRANCH}" + +# Sync lockfile +UV_LOCKED=0 uv sync --all-groups + +# Commit only bumped files + lockfile +git add pyproject.toml gitfluence/__init__.py CHANGELOG.md uv.lock +git commit --no-edit -m "Bump version: ${VERSION}" + +# Push and create PR +git push --set-upstream origin "${BRANCH}" +gh pr create --title "${PR_TITLE}" --body "${PR_TITLE}" + +# Wait for CI checks to register (max 30s) +echo "==> Waiting for CI checks to be registered..." +for i in $(seq 1 30); do + sleep 1 + gh pr checks && RC=$? || RC=$? + if [[ $RC -eq 0 || $RC -eq 8 ]]; then + break + fi +done + +if [[ $RC -ne 0 && $RC -ne 8 ]]; then + echo "ERROR: No CI checks appeared after 30s. Abort." >&2 + exit 1 +fi + +# Watch checks until complete, then merge on success +echo "==> Watching CI checks..." +gh pr checks --watch --interval 1 --fail-fast +echo "==> Merging PR..." +gh pr merge --squash --delete-branch diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh deleted file mode 100755 index 60144bb..0000000 --- a/scripts/prepare-release.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# Create a release branch from the current (already bumped) version, -# sync lockfile, commit, push, open PR, and watch CI checks. -# -# Usage: ./scripts/prepare-release.sh -# -# Prerequisites: -# - bump-my-version bump has already been run (without --commit) -# - Working directory is the repo root -# - gh CLI is authenticated - -set -euo pipefail - -VERSION="$(bump-my-version show current_version)" -if [[ -z "$VERSION" ]]; then - echo "ERROR: Could not determine current version" >&2 - exit 1 -fi - -# Detect if this is an RC version -if [[ "$VERSION" =~ -rc[0-9]+$ ]]; then - BRANCH="chore/bump-${VERSION}" - PR_TITLE="Bumping version to ${VERSION}" - # Revert RC heading in CHANGELOG.md — keep only ## Unreleased - if ! python scripts/revert_changelog_rc.py; then - echo "WARNING: revert_changelog_rc.py failed" >&2 - fi -else - BRANCH="chore/release-${VERSION}" - PR_TITLE="Release ${VERSION}" -fi - -echo "==> Preparing release for v${VERSION} on branch ${BRANCH}" - -# Create branch -git checkout -b "${BRANCH}" - -# Sync lockfile -UV_LOCKED=0 uv sync --all-groups - -# Commit all bumped files + lockfile -git add -A -git commit --no-edit -m "chore: bump version to ${VERSION}" - -# Push and create PR -git push --set-upstream origin "${BRANCH}" -gh pr create --title "${PR_TITLE}" --body "${PR_TITLE}" - -echo "==> Watching CI checks..." -gh pr checks --watch -echo "==> CI passed. Ready to merge." diff --git a/scripts/commit-release.sh b/scripts/publish-release.sh similarity index 82% rename from scripts/commit-release.sh rename to scripts/publish-release.sh index 66cfbbb..9563dbc 100755 --- a/scripts/commit-release.sh +++ b/scripts/publish-release.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash # After PR is merged: checkout main, tag the current version, push tag. # -# Usage: ./scripts/commit-release.sh +# Usage: ./scripts/publish-release.sh # # Prerequisites: # - The version-bump PR has been merged to main set -euo pipefail -git checkout main +DEFAULT_BRANCH="$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" +git checkout "$DEFAULT_BRANCH" git pull VERSION="$(bump-my-version show current_version)"