Skip to content

Build release

Build release #11

Workflow file for this run

name: Build release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to publish, for example v0.1.0"
required: true
type: string
permissions:
contents: write
pull-requests: read
concurrency:
group: release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
validate:
name: Validate
uses: ./.github/workflows/validate.yml
with:
ref: ${{ github.event.inputs.tag || github.ref }}
build-package: false
wheel:
name: Build wheel
runs-on: ubuntu-24.04
env:
IMPORT_NAME: src_py_lib
PYTHON_VERSION: "3.11"
UV_VERSION: "0.11.7"
steps:
- name: Check out release ref
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache uv
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
- name: Install build tools
run: python -m pip install "uv==${UV_VERSION}"
- name: Validate release inputs
id: release
run: |
release_tag="${{ github.event.inputs.tag || github.ref_name }}"
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
fi
if ! git rev-parse --verify --quiet "refs/tags/${release_tag}" >/dev/null; then
echo "::error title=Missing tag::Tag '${release_tag}' was not fetched. Create and push it before running this workflow."
exit 1
fi
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}'."
exit 1
fi
echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
- name: Build distributions
id: build
run: |
dist_dir="build/release/dist"
rm -rf build/release
mkdir -p "${dist_dir}"
shopt -s nullglob
uv build --wheel --sdist --out-dir "${dist_dir}" --no-create-gitignore
project_wheels=("${dist_dir}"/*.whl)
if [[ "${#project_wheels[@]}" -ne 1 ]]; then
echo "::error title=Unexpected wheel count::Expected one project wheel, found ${#project_wheels[@]}."
exit 1
fi
source_distributions=("${dist_dir}"/*.tar.gz)
if [[ "${#source_distributions[@]}" -ne 1 ]]; then
echo "::error title=Unexpected source distribution count::Expected one source distribution, found ${#source_distributions[@]}."
exit 1
fi
wheel_path="${project_wheels[0]}"
wheel_name="$(basename "${wheel_path}")"
source_distribution_path="${source_distributions[0]}"
source_distribution_name="$(basename "${source_distribution_path}")"
wheel_checksum_path="${wheel_path}.sha256"
source_distribution_checksum_path="${source_distribution_path}.sha256"
(
cd "$(dirname "${wheel_path}")"
shasum -a 256 "${wheel_name}" > "$(basename "${wheel_checksum_path}")"
shasum -a 256 "${source_distribution_name}" > "$(basename "${source_distribution_checksum_path}")"
)
{
echo "wheel_path=${wheel_path}"
echo "wheel_name=${wheel_name}"
echo "source_distribution_path=${source_distribution_path}"
echo "source_distribution_name=${source_distribution_name}"
echo "wheel_checksum_path=${wheel_checksum_path}"
echo "source_distribution_checksum_path=${source_distribution_checksum_path}"
} >> "${GITHUB_OUTPUT}"
- name: Smoke test installed wheel
run: |
python -m venv build/release/install-venv
. build/release/install-venv/bin/activate
python -m pip install "${{ steps.build.outputs.wheel_path }}"
python - <<'PY'
import os
import src_py_lib
if src_py_lib.__name__ != os.environ["IMPORT_NAME"]:
raise SystemExit(f"unexpected import name: {src_py_lib.__name__}")
PY
- name: Write release notes
id: notes
run: |
release_tag="${{ steps.release.outputs.tag }}"
wheel_name="${{ steps.build.outputs.wheel_name }}"
source_distribution_name="${{ steps.build.outputs.source_distribution_name }}"
notes_path="build/release/release-notes.md"
cat > "${notes_path}" <<EOF
## Install
Install from PyPI:
\`\`\`sh
pip install src-py-lib
\`\`\`
Install from the release wheel:
\`\`\`sh
pip install "https://github.com/sourcegraph/src-py-lib/releases/download/${release_tag}/${wheel_name}"
\`\`\`
Source distribution:
\`\`\`sh
curl -LO "https://github.com/sourcegraph/src-py-lib/releases/download/${release_tag}/${source_distribution_name}"
\`\`\`
Or install this tag with uv:
\`\`\`sh
uv add "git+https://github.com/sourcegraph/src-py-lib.git@${release_tag}"
\`\`\`
Verify downloaded assets with the matching \`.sha256\` files.
EOF
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
- name: Upload workflow artifact
uses: actions/upload-artifact@v7
with:
name: src-py-lib-release
path: |
${{ steps.build.outputs.wheel_path }}
${{ steps.build.outputs.source_distribution_path }}
${{ steps.build.outputs.wheel_checksum_path }}
${{ steps.build.outputs.source_distribution_checksum_path }}
${{ steps.notes.outputs.path }}
- name: Upload PyPI artifact
uses: actions/upload-artifact@v7
with:
name: pypi-distributions
path: |
${{ steps.build.outputs.wheel_path }}
${{ steps.build.outputs.source_distribution_path }}
github-release:
name: Publish GitHub release assets
needs: [validate, wheel]
runs-on: ubuntu-24.04
steps:
- name: Download release assets
uses: actions/download-artifact@v7
with:
name: src-py-lib-release
path: release-assets
- name: Publish GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
release_tag="${{ github.event.inputs.tag || github.ref_name }}"
notes_path="$(find release-assets -name release-notes.md -print -quit)"
mapfile -t release_assets < <(find release-assets -type f ! -name release-notes.md | sort)
if [[ -z "${notes_path}" ]]; then
echo "::error title=Missing release notes::release-notes.md was not found in release artifact."
exit 1
fi
if [[ "${#release_assets[@]}" -eq 0 ]]; then
echo "::error title=Missing release assets::No release assets were downloaded."
exit 1
fi
if gh release view "${release_tag}" >/dev/null 2>&1; then
gh release edit "${release_tag}" --title "${release_tag}" --notes-file "${notes_path}"
gh release upload "${release_tag}" "${release_assets[@]}" --clobber
else
gh release create "${release_tag}" \
"${release_assets[@]}" \
--title "${release_tag}" \
--notes-file "${notes_path}" \
--verify-tag
fi
pypi:
name: Publish PyPI package
needs: [validate, wheel]
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
environment:
name: pypi
url: https://pypi.org/project/src-py-lib/
steps:
- name: Download built distribution
uses: actions/download-artifact@v7
with:
name: pypi-distributions
path: dist
- name: Publish PyPI package
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true