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
106 changes: 76 additions & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,59 @@ 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

permissions:
contents: write
contents: read
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

wheelhouse:
name: ${{ matrix.platform }}-py311 wheelhouse
needs: release_ref
runs-on: ${{ matrix.runs_on }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -61,7 +87,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 @@ -81,8 +107,10 @@ jobs:

- name: Validate release inputs
id: release
env:
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
run: |
release_tag="${{ github.event.inputs.tag || github.ref_name }}"
release_tag="${RELEASE_TAG}"
if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error title=Invalid release tag::Use a vMAJOR.MINOR.PATCH tag, got '${release_tag}'."
exit 1
Expand All @@ -100,19 +128,8 @@ jobs:
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}'."
exit 1
fi

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

- name: Validate runner architecture
run: |
Expand All @@ -131,6 +148,7 @@ jobs:
id: build
run: |
release_tag="${{ steps.release.outputs.tag }}"
release_version="${{ steps.release.outputs.version }}"
release_dir="build/release/${ASSET_BASENAME}"
wheelhouse_dir="${release_dir}/wheelhouse"
dist_dir="build/release/dist"
Expand Down Expand Up @@ -158,25 +176,50 @@ jobs:
project_wheel_name="$(basename "${project_wheel_path}")"
project_source_distribution_path="${project_source_distributions[0]}"
project_source_distribution_name="$(basename "${project_source_distribution_path}")"
case "${project_wheel_name}" in
src_auth_perms_sync-"${release_version}"-*.whl)
;;
*)
echo "::error title=Wheel version mismatch::Expected wheel version ${release_version}, got '${project_wheel_name}'."
exit 1
;;
esac
case "${project_source_distribution_name}" in
src_auth_perms_sync-"${release_version}".tar.gz)
;;
*)
echo "::error title=Source distribution version mismatch::Expected source distribution version ${release_version}, got '${project_source_distribution_name}'."
exit 1
;;
esac
project_wheel_checksum_path="${project_wheel_path}.sha256"
project_source_distribution_checksum_path="${project_source_distribution_path}.sha256"
if [[ ! -f "${project_wheel_path}" ]]; then
echo "::error title=Missing project wheel::Expected ${project_wheel_path} to exist."
exit 1
fi

uv export \
--no-dev \
--no-emit-project \
--no-hashes \
--no-header \
--no-annotate \
--frozen \
--output-file "${requirements_file}"
dependency_metadata_dir="$(mktemp -d)"
git clone --no-hardlinks . "${dependency_metadata_dir}" >/dev/null
(
cd "${dependency_metadata_dir}"
git checkout --detach "${release_tag}" >/dev/null
mkdir -p "$(dirname "${requirements_file}")"
uv export \
--no-sources \
--no-dev \
--no-emit-project \
--no-hashes \
--no-header \
--no-annotate \
--output-file "${requirements_file}"
)

cp "${dependency_metadata_dir}/${requirements_file}" "${requirements_file}"
cp "${requirements_file}" "${runtime_requirements_file}"
if grep -q '^\./' "${runtime_requirements_file}"; then
if grep -Eq '(^-e[[:space:]]|^(\.\.?/)|(^|[[:space:]])file:| @ (\.\.?/|file:))' "${runtime_requirements_file}"; then
echo "::error title=Unexpected local dependency::Runtime requirements must resolve from PyPI."
cat "${runtime_requirements_file}"
exit 1
fi

Expand Down Expand Up @@ -356,8 +399,10 @@ jobs:

github-release:
name: Publish GitHub release assets
needs: [validate, wheelhouse]
needs: [release_ref, validate, wheelhouse]
runs-on: ubuntu-24.04
permissions:
contents: write

steps:
- name: Download wheelhouse artifacts
Expand All @@ -377,8 +422,9 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
run: |
release_tag="${{ github.event.inputs.tag || github.ref_name }}"
release_tag="${RELEASE_TAG}"
notes_path="$(find release-notes -name release-notes.md -print -quit)"
mapfile -t release_assets < <(find release-assets -type f | sort)

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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 @@ -208,6 +209,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.ref || github.ref }}

Expand All @@ -227,8 +229,8 @@ jobs:
- name: Install uv
run: python -m pip install "uv==${UV_VERSION}"

- name: Build wheel
run: uv build --wheel --out-dir dist --no-create-gitignore
- name: Build distributions
run: uv build --wheel --sdist --out-dir dist --no-create-gitignore

- name: Smoke test installed wheel
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ __pycache__
*.gql
*.py[cod]
*.py[oc]
*.swp
*.yaml
build/
dist/
logs*/
logs/
src-auth-perms-sync-runs/
wheels/
Expand Down
104 changes: 24 additions & 80 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,50 +44,27 @@ uv run src-auth-perms-sync --restore backups/<source>/<run>/before.json

## Release process

- The tagged source commit must already contain the package version it
releases. Do not make the customer release workflow edit `pyproject.toml`.
- Prepare the version bump on a branch. Set `VERSION`, then copy / paste:
- 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`.
- Validate the remote head of `main` before tagging it:

```bash
set -euo pipefail

VERSION=0.2.1
BRANCH="release-v${VERSION}"
VERSION_INPUT=<next-version>
VERSION="${VERSION_INPUT#v}"

[[ "${VERSION_INPUT}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]
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 the release candidate before opening / merging the PR:

```bash
set -euo pipefail
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)"

uv lock --check
actionlint
Expand All @@ -97,57 +74,24 @@ uv run pyright
uv run python -m unittest discover -s tests
uv run src-auth-perms-sync --help
npx --yes markdownlint-cli2@0.22.1
uv build --wheel --out-dir /tmp/src-auth-perms-sync-release-check --no-create-gitignore
uv build --wheel --sdist --out-dir /tmp/src-auth-perms-sync-release-check --no-create-gitignore
rm -rf /tmp/src-auth-perms-sync-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.
- Tag the remote head of `main` directly:

```bash
set -euo pipefail

VERSION=0.2.1
BRANCH="release-v${VERSION}"
VERSION_INPUT=<next-version>
VERSION="${VERSION_INPUT#v}"
GH_REPO="sourcegraph/src-auth-perms-sync"

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-auth-perms-sync 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 feature branch commit.

```bash
set -euo pipefail

VERSION=0.2.1

[[ "${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 customer release workflow and confirm the GitHub release assets
are uploaded:

```bash
set -euo pipefail

VERSION=0.2.1
GH_REPO="sourcegraph/src-auth-perms-sync"

RUN_ID="$(
gh run list \
Expand All @@ -169,13 +113,13 @@ gh release view "v${VERSION}" --repo "${GH_REPO}"
```bash
set -euo pipefail

VERSION=0.2.1
VERSION_INPUT=<version-to-fix>
VERSION="${VERSION_INPUT#v}"
GH_REPO="sourcegraph/src-auth-perms-sync"

[[ "${VERSION_INPUT}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]
git fetch origin --tags --prune
git switch main
git pull --ff-only
git tag -f "v${VERSION}" origin/main
git tag -f -a "v${VERSION}" origin/main -m "Release v${VERSION}"
git push origin "refs/tags/v${VERSION}" --force

RUN_ID="$(
Expand Down
Loading