Build release #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |