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
78 changes: 55 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ on:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to publish, for example v0.1.0"
version:
description: "Package version to publish, for example 0.1.0 or v0.1.0"
required: true
type: string

Expand All @@ -16,23 +16,49 @@ permissions:
pull-requests: read

concurrency:
group: release-${{ github.event.inputs.tag || github.ref_name }}
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
release_ref:
name: Resolve release tag
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}

steps:
- name: Resolve release tag
id: release
env:
RELEASE_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
run: |
release_input="${RELEASE_INPUT}"
if [[ ! "${release_input}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error title=Invalid release version::Use MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH, got '${release_input}'."
exit 1
fi

release_version="${release_input#v}"
release_tag="v${release_version}"
echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
echo "version=${release_version}" >> "${GITHUB_OUTPUT}"

validate:
name: Validate
needs: release_ref
uses: ./.github/workflows/validate.yml
with:
ref: ${{ github.event.inputs.tag || github.ref }}
ref: ${{ needs.release_ref.outputs.tag }}
build-package: false

wheel:
name: Build wheel
needs: release_ref
runs-on: ubuntu-24.04
env:
IMPORT_NAME: src_py_lib
Expand All @@ -45,7 +71,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.inputs.tag || github.ref }}
ref: ${{ needs.release_ref.outputs.tag }}

- name: Set up Python
uses: actions/setup-python@v6
Expand All @@ -66,7 +92,7 @@ jobs:
- name: Validate release inputs
id: release
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
run: |
release_tag="${RELEASE_TAG}"
if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
Expand All @@ -80,29 +106,19 @@ jobs:
tag_revision="$(git rev-list -n 1 "${release_tag}")"
git fetch --no-tags origin main
main_revision="$(git rev-parse origin/main)"
if ! git merge-base --is-ancestor "${tag_revision}" "${main_revision}"; then
echo "::error title=Tag is not on main::Tag '${release_tag}' points at ${tag_revision}, which is not reachable from origin/main."
echo "::error::Merge the release PR first, then tag the main commit."
exit 1
fi

project_version=$(uv run --frozen python - <<'PY'
import tomllib

with open("pyproject.toml", "rb") as pyproject_file:
print(tomllib.load(pyproject_file)["project"]["version"])
PY
)
if [[ "v${project_version}" != "${release_tag}" ]]; then
echo "::error title=Version mismatch::pyproject.toml version '${project_version}' does not match tag '${release_tag}'."
if [[ "${tag_revision}" != "${main_revision}" ]]; then
echo "::error title=Tag is not origin/main::Tag '${release_tag}' points at ${tag_revision}, but origin/main is ${main_revision}."
echo "::error::Tag the remote head of main, then rerun the release."
exit 1
fi

echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
echo "version=${release_tag#v}" >> "${GITHUB_OUTPUT}"

- name: Build distributions
id: build
run: |
release_version="${{ steps.release.outputs.version }}"
dist_dir="build/release/dist"
rm -rf build/release
mkdir -p "${dist_dir}"
Expand All @@ -123,6 +139,22 @@ jobs:
wheel_name="$(basename "${wheel_path}")"
source_distribution_path="${source_distributions[0]}"
source_distribution_name="$(basename "${source_distribution_path}")"
case "${wheel_name}" in
src_py_lib-"${release_version}"-*.whl)
;;
*)
echo "::error title=Wheel version mismatch::Expected wheel version ${release_version}, got '${wheel_name}'."
exit 1
;;
esac
case "${source_distribution_name}" in
src_py_lib-"${release_version}".tar.gz)
;;
*)
echo "::error title=Source distribution version mismatch::Expected source distribution version ${release_version}, got '${source_distribution_name}'."
exit 1
;;
esac
wheel_checksum_path="${wheel_path}.sha256"
source_distribution_checksum_path="${source_distribution_path}.sha256"

Expand Down Expand Up @@ -214,7 +246,7 @@ jobs:

github-release:
name: Publish GitHub release assets
needs: [validate, wheel]
needs: [release_ref, validate, wheel]
runs-on: ubuntu-24.04
permissions:
contents: write
Expand All @@ -230,7 +262,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
run: |
release_tag="${RELEASE_TAG}"
notes_path="$(find release-assets -name release-notes.md -print -quit)"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.ref || github.ref }}

Expand Down Expand Up @@ -217,6 +218,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.ref || github.ref }}

Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ __pycache__
*.gql
*.py[cod]
*.py[oc]
*.swp
*.yaml
build/
dist/
wheels/

# Allow
!.env.example
!.markdownlint-cli2.yaml
!.markdownlint-cli2.yaml
111 changes: 14 additions & 97 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,111 +51,28 @@ uv run python -m unittest discover -s tests

## Release process

- The tagged source commit must already contain the package version it
releases. Do not make the release workflow edit `pyproject.toml`.
- The tag must be `vMAJOR.MINOR.PATCH`, and `.github/workflows/release.yml`
verifies that it matches `project.version` before building GitHub release
assets and publishing to PyPI.
- Prepare releases on a branch from current `main`. Set `VERSION`, then run:
- As part of every release bump, find old release-version literals in
`AGENTS.md`, `README.md`, and release snippets, and replace them with the
new version where they are meant to stay current.
- Package versions are derived from Git tags through `hatch-vcs`.
- `pyproject.toml` must use `dynamic = ["version"]`; do not add a hard-coded
`project.version` for releases.
- The release tag must be `vMAJOR.MINOR.PATCH` and point at a commit reachable
from `origin/main`.
- The release workflow builds from the tag and checks that wheel and source
distribution filenames match the tag version before publishing.
- Do not make the release workflow edit `pyproject.toml` or `uv.lock`.
- Tag the remote head of `main` directly:

```sh
set -euo pipefail

VERSION=0.1.6
BRANCH="release-v${VERSION}"

git fetch origin --tags --prune
git switch main
git pull --ff-only
git switch -c "${BRANCH}"

uv run python - "${VERSION}" <<'PY'
from pathlib import Path
import re
import sys

version = sys.argv[1]
path = Path("pyproject.toml")
text = path.read_text()
new_text = re.sub(
r'(?m)^version = "[^"]+"$',
f'version = "{version}"',
text,
count=1,
)
if new_text == text:
raise SystemExit("pyproject.toml version was not updated")
path.write_text(new_text)
PY

uv lock
```

- Validate before opening the PR:

```sh
set -euo pipefail

uv lock --check
actionlint
npx --yes markdownlint-cli2@0.22.1
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run python -m unittest discover -s tests
uv build --wheel --sdist --out-dir /tmp/src-py-lib-release-check --no-create-gitignore
rm -rf /tmp/src-py-lib-release-check
```

- Commit, push, open the PR, wait for checks, then merge it. If review is
required, stop after `gh pr checks` and ask for review before merging.

```sh
set -euo pipefail

VERSION=0.1.6
BRANCH="release-v${VERSION}"
VERSION_INPUT=<next-version>
VERSION="${VERSION_INPUT#v}"
GH_REPO="sourcegraph/src-py-lib"

git add pyproject.toml uv.lock
git commit -m "Release v${VERSION}"
git push -u origin "${BRANCH}"

gh pr create \
--repo "${GH_REPO}" \
--base main \
--head "${BRANCH}" \
--title "Release v${VERSION}" \
--body "Bump src-py-lib package metadata to ${VERSION}."

gh pr checks "${BRANCH}" --repo "${GH_REPO}" --watch --fail-fast
gh pr merge "${BRANCH}" --repo "${GH_REPO}" --squash --delete-branch
```

- Tag the merged `main` commit. Do not tag a branch commit.

```sh
set -euo pipefail

VERSION=0.1.6

[[ "${VERSION_INPUT}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]
git fetch origin --tags --prune
git switch main
git pull --ff-only
git tag "v${VERSION}"
MAIN_COMMIT="$(git rev-parse origin/main)"
git tag -a "v${VERSION}" "${MAIN_COMMIT}" -m "Release v${VERSION}"
git push origin "v${VERSION}"
```

- Watch the release workflow and confirm the GitHub release and PyPI project.

```sh
set -euo pipefail

VERSION=0.1.6
GH_REPO="sourcegraph/src-py-lib"

RUN_ID="$(
gh run list \
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[dependency-groups]
Expand All @@ -10,7 +10,7 @@ dev = [

[project]
name = "src-py-lib"
version = "0.1.7"
dynamic = ["version"]
description = "Reusable libraries for Sourcegraph projects"
readme = "README.md"
requires-python = ">=3.11"
Expand Down Expand Up @@ -44,6 +44,9 @@ Issues = "https://github.com/sourcegraph/src-py-lib/issues"
[tool.hatch.build.targets.wheel]
packages = ["src/src_py_lib"]

[tool.hatch.version]
source = "vcs"

[tool.pyright]
include = ["src/src_py_lib", "tests"]
typeCheckingMode = "strict"
Expand Down
1 change: 0 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.