Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build and publish to TestPyPI
on:
push:
tags:
- "v*"
- "v*-rc*"
workflow_dispatch:

permissions:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
63 changes: 22 additions & 41 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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<version>`, and pushes. Triggers `deploy-test.yml`: **build → publish to TestPyPI**.
Expand All @@ -109,10 +96,10 @@ Check the package page at `https://test.pypi.org/project/gitfluence/<VERSION>/`
- 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==<VERSION>
uv pip install --extra-index-url https://test.pypi.org/simple/ --index-strategy unsafe-best-match gitfluence==<VERSION>
```

### 6. Iterate if needed
Expand All @@ -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:
Expand All @@ -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**.
Expand All @@ -176,10 +157,10 @@ uv pip install gitfluence==<VERSION>

## 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

Expand Down
86 changes: 86 additions & 0 deletions scripts/merge-bump.sh
Original file line number Diff line number Diff line change
@@ -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
51 changes: 0 additions & 51 deletions scripts/prepare-release.sh

This file was deleted.

5 changes: 3 additions & 2 deletions scripts/commit-release.sh → scripts/publish-release.sh
Original file line number Diff line number Diff line change
@@ -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)"
Expand Down
Loading