Skip to content

Improve release process (#6) #5

Improve release process (#6)

Improve release process (#6) #5

Workflow file for this run

name: Build customer 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
concurrency:
group: release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
wheelhouse:
name: ${{ matrix.platform }}-py311 wheelhouse
runs-on: ${{ matrix.runs_on }}
strategy:
fail-fast: false
# The first matrix leg creates the release; later legs upload more assets.
max-parallel: 1
matrix:
include:
- platform: linux-x86_64
runs_on: ubuntu-24.04
asset_basename: src-auth-perms-sync-linux-x64
target_description: Linux x64
expected_machine: x86_64
- platform: macos-arm64
runs_on: macos-26
asset_basename: src-auth-perms-sync-macos-arm64
target_description: macOS arm64
expected_machine: arm64
env:
ASSET_BASENAME: ${{ matrix.asset_basename }}
PACKAGE_NAME: src-auth-perms-sync
PYTHON_VERSION: "3.11"
TARGET_DESCRIPTION: ${{ matrix.target_description }}
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 }}
cache: pip
- name: Install build tools
run: |
python -m pip install --upgrade pip
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: Validate package
run: |
actual_machine=$(uv run --frozen python - <<'PY'
import platform
print(platform.machine())
PY
)
if [[ "${actual_machine}" != "${{ matrix.expected_machine }}" ]]; then
echo "::error title=Wrong runner architecture::Expected ${{ matrix.expected_machine }}, got ${actual_machine}."
exit 1
fi
uv lock --check
uv run --frozen ruff check src/src_auth_perms_sync/
uv run --frozen ruff format --check src/src_auth_perms_sync/
uv run --frozen pyright
uv run --frozen src-auth-perms-sync --help >/tmp/src-auth-perms-sync-help.txt
- name: Build wheelhouse tarball
id: build
run: |
release_tag="${{ steps.release.outputs.tag }}"
release_dir="build/release/${ASSET_BASENAME}"
wheelhouse_dir="${release_dir}/wheelhouse"
dist_dir="build/release/dist"
requirements_file="build/release/requirements.txt"
runtime_requirements_file="build/release/runtime-requirements.txt"
asset_path="build/release/${ASSET_BASENAME}.tar.gz"
checksum_path="${asset_path}.sha256"
rm -rf build/release
mkdir -p "${wheelhouse_dir}" "${dist_dir}"
uv build --wheel --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
project_wheel_path="${project_wheels[0]}"
project_wheel_name="$(basename "${project_wheel_path}")"
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}"
cp "${requirements_file}" "${runtime_requirements_file}"
if grep -q '^\./' "${runtime_requirements_file}"; then
echo "::error title=Unexpected local dependency::Runtime requirements must resolve from PyPI."
exit 1
fi
python -m pip wheel \
--only-binary=:all: \
--wheel-dir "${wheelhouse_dir}" \
--requirement "${runtime_requirements_file}"
cp "${project_wheel_path}" "${wheelhouse_dir}/"
src_py_lib_wheels=("${wheelhouse_dir}"/src_py_lib-*.whl)
if [[ "${#src_py_lib_wheels[@]}" -ne 1 ]]; then
echo "::error title=Unexpected src-py-lib wheel count::Expected one src-py-lib wheel, found ${#src_py_lib_wheels[@]}."
exit 1
fi
cat > "${wheelhouse_dir}/INSTALL.txt" <<EOF
# src-auth-perms-sync ${release_tag} offline install
This wheelhouse targets ${TARGET_DESCRIPTION} with Python 3.11.
tar -xzf ${ASSET_BASENAME}.tar.gz
python3.11 -m venv .venv
. .venv/bin/activate
pip install --no-index --find-links ./wheelhouse ${PACKAGE_NAME}
src-auth-perms-sync --help
Connected install from PyPI:
pip install ${PACKAGE_NAME}
GitHub release asset install, using the same project wheel uploaded to PyPI:
pip install "https://github.com/sourcegraph/src-auth-perms-sync/releases/download/${release_tag}/${project_wheel_name}"
EOF
(cd "${wheelhouse_dir}" && shasum -a 256 *.whl > WHEELS.sha256)
test -f "${project_wheel_path}"
test -f "${wheelhouse_dir}"/src_auth_perms_sync-*.whl
test -f "${wheelhouse_dir}"/src_py_lib-*.whl
if find "${wheelhouse_dir}" -type f \
! -name '*.whl' \
! -name INSTALL.txt \
! -name WHEELS.sha256 \
| grep .; then
echo "::error title=Unexpected wheelhouse content::Wheelhouse contains non-wheel payloads."
exit 1
fi
tar -C "${release_dir}" -czf "${asset_path}" wheelhouse
(
cd "$(dirname "${asset_path}")"
shasum -a 256 "$(basename "${asset_path}")" > "$(basename "${checksum_path}")"
)
echo "asset_path=${asset_path}" >> "${GITHUB_OUTPUT}"
echo "checksum_path=${checksum_path}" >> "${GITHUB_OUTPUT}"
echo "project_wheel_path=${project_wheel_path}" >> "${GITHUB_OUTPUT}"
echo "project_wheel_name=${project_wheel_name}" >> "${GITHUB_OUTPUT}"
- name: Validate offline install from tarball
run: |
validation_dir=$(mktemp -d)
tar -xzf "${{ steps.build.outputs.asset_path }}" -C "${validation_dir}"
python3.11 -m venv "${validation_dir}/.venv"
. "${validation_dir}/.venv/bin/activate"
PIP_CACHE_DIR="${validation_dir}/pip-cache" \
GIT_ALLOW_PROTOCOL=file \
pip install \
--no-cache-dir \
--no-index \
--find-links "${validation_dir}/wheelhouse" \
"${PACKAGE_NAME}"
src-auth-perms-sync --help >/tmp/src-auth-perms-sync-release-help.txt
- name: Write release notes
id: notes
run: |
release_tag="${{ steps.release.outputs.tag }}"
project_wheel_name="${{ steps.build.outputs.project_wheel_name }}"
notes_path="build/release/release-notes.md"
cat > "${notes_path}" <<EOF
## Customer install
### Restricted/offline Linux x64 + Python 3.11 install
Download \`src-auth-perms-sync-linux-x64.tar.gz\`, then run:
\`\`\`sh
tar -xzf src-auth-perms-sync-linux-x64.tar.gz
python3.11 -m venv .venv
. .venv/bin/activate
pip install --no-index --find-links ./wheelhouse ${PACKAGE_NAME}
src-auth-perms-sync --help
\`\`\`
### Restricted/offline macOS arm64 + Python 3.11 install
Download \`src-auth-perms-sync-macos-arm64.tar.gz\`, then run:
\`\`\`sh
tar -xzf src-auth-perms-sync-macos-arm64.tar.gz
python3.11 -m venv .venv
. .venv/bin/activate
pip install --no-index --find-links ./wheelhouse ${PACKAGE_NAME}
src-auth-perms-sync --help
\`\`\`
The tarball includes this project, \`src-py-lib\`, and all runtime wheels.
Verify the download with the matching \`.sha256\` file.
### Connected PyPI install
\`\`\`sh
pip install ${PACKAGE_NAME}
\`\`\`
### GitHub release asset install
\`\`\`sh
pip install "https://github.com/sourcegraph/src-auth-perms-sync/releases/download/${release_tag}/${project_wheel_name}"
\`\`\`
EOF
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
- name: Upload workflow artifact
uses: actions/upload-artifact@v7
with:
name: ${{ env.ASSET_BASENAME }}
path: |
${{ steps.build.outputs.asset_path }}
${{ steps.build.outputs.checksum_path }}
${{ steps.build.outputs.project_wheel_path }}
${{ steps.notes.outputs.path }}
- name: Upload PyPI artifact
if: matrix.platform == 'linux-x86_64'
uses: actions/upload-artifact@v7
with:
name: pypi-distributions
path: ${{ steps.build.outputs.project_wheel_path }}
- name: Publish GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
release_tag="${{ steps.release.outputs.tag }}"
asset_path="${{ steps.build.outputs.asset_path }}"
checksum_path="${{ steps.build.outputs.checksum_path }}"
project_wheel_path="${{ steps.build.outputs.project_wheel_path }}"
notes_path="${{ steps.notes.outputs.path }}"
release_assets=("${asset_path}" "${checksum_path}")
if [[ "${{ matrix.platform }}" == "linux-x86_64" ]]; then
release_assets+=("${project_wheel_path}")
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: wheelhouse
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
environment:
name: pypi
url: https://pypi.org/project/src-auth-perms-sync/
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