diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a18bac3..ce22abe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -16,7 +16,7 @@ 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: @@ -24,15 +24,41 @@ defaults: 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 @@ -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 @@ -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 @@ -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}" @@ -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" @@ -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 @@ -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)" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 38c7363..7714adb 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 }} @@ -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 }} diff --git a/.gitignore b/.gitignore index 8d17076..37c9c71 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__ *.gql *.py[cod] *.py[oc] +*.swp *.yaml build/ dist/ @@ -17,4 +18,4 @@ wheels/ # Allow !.env.example -!.markdownlint-cli2.yaml +!.markdownlint-cli2.yaml \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index d35c732..b609a79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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= +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 \ diff --git a/pyproject.toml b/pyproject.toml index de5e138..5b21d3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [dependency-groups] @@ -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" @@ -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" diff --git a/uv.lock b/uv.lock index 5e5ccf9..f13ce62 100644 --- a/uv.lock +++ b/uv.lock @@ -254,7 +254,6 @@ wheels = [ [[package]] name = "src-py-lib" -version = "0.1.7" source = { editable = "." } dependencies = [ { name = "httpx" },