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
306 changes: 306 additions & 0 deletions .github/workflows/pygosqlx-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# Build, test and publish pygosqlx (python/) wheels to PyPI.
#
# What runs when:
# pull_request (python/**, pkg/**, go.mod, go.sum, this file)
# -> build + test wheels and sdist only. Nothing is published.
# pkg/** because pkg/cbinding links the whole parser via pkg/gosqlx.
# push of a tag matching 'pygosqlx-v*' (e.g. pygosqlx-v0.3.0)
# -> build + test, then publish to PyPI. The tag must match the version
# in python/pyproject.toml and must point at a commit on main.
# This is the ONLY path to PyPI, so every published version has a tag.
# workflow_dispatch
# -> build + test, then publish to the index chosen by the 'target'
# input: 'testpypi' (default) or 'none' (build only). Publishing
# from a dispatch is only allowed on main. TestPyPI skips files that
# already exist, so repeated dry runs of one version do not fail.
#
# Tag isolation: the main GoSQLX 'v*' release tags never trigger this
# workflow, and pygosqlx-v* tags must not disturb the Go release:
# release.yml pins GoReleaser's current/previous tag to the 'v*' series, and
# release.yml, vscode-publish.yml and bench-compare.yml filter on 'v*'.
#
# Wheels: one py3-none-<platform> wheel per OS/arch (the bundled libgosqlx is
# loaded through ctypes and does not depend on the CPython ABI), each tested
# on CPython 3.10-3.14 against the installed wheel:
# linux x86_64, aarch64 (built in manylinux_2_28, native runners;
# auditwheel repair pins manylinux_2_17)
# macOS x86_64, arm64 (macOS 13+)
# windows AMD64
#
# Publishing uses PyPI Trusted Publishing (OIDC); there is no API token.
# ONE-TIME MAINTAINER SETUP (not automated, do this before the first tag):
# 1. On https://pypi.org/manage/account/publishing/ add a *pending* trusted
# publisher: PyPI project name 'pygosqlx', owner 'ajitpratap0',
# repository 'GoSQLX', workflow 'pygosqlx-release.yml', environment
# 'pypi'.
# 2. In GitHub repo Settings -> Environments create an environment named
# 'pypi' (recommended: deployment policy limited to tags 'pygosqlx-v*'
# and a required reviewer). Also recommended: a tag ruleset restricting
# who may create 'pygosqlx-v*' tags.
# 3. Optional, for dry runs: repeat 1 on https://test.pypi.org with
# environment 'testpypi', and create a 'testpypi' GitHub environment.
# Then release with: git tag -a pygosqlx-vX.Y.Z && git push origin pygosqlx-vX.Y.Z

name: pygosqlx Release

on:
pull_request:
branches: [ main ]
paths:
- 'python/**'
- 'pkg/**'
- 'go.mod'
- 'go.sum'
- '.github/workflows/pygosqlx-release.yml'
push:
tags:
- 'pygosqlx-v*'
workflow_dispatch:
inputs:
target:
description: 'Where to publish the built distributions'
type: choice
options:
- testpypi
- none
default: testpypi

permissions:
contents: read

concurrency:
group: pygosqlx-release-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
# Go toolchain for the macOS and Windows runners. Must equal GO_VERSION in
# python/scripts/install-go-linux.sh (used in manylinux); the sdist job
# fails if they differ.
GO_VERSION: '1.26.10'

jobs:
wheels:
name: wheels (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64
os: ubuntu-24.04
arch: x86_64
- name: linux-aarch64
os: ubuntu-24.04-arm
arch: aarch64
- name: macos-x86_64
os: macos-15-intel
- name: macos-arm64
os: macos-15
- name: windows-amd64
os: windows-2025

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

# Linux builds run inside the manylinux container and install Go there
# (CIBW_BEFORE_ALL_LINUX); macOS and Windows build on the runner.
- name: Set up Go
if: runner.os != 'Linux'
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ${{ env.GO_VERSION }}
cache: false

- name: Build and test wheels
uses: pypa/cibuildwheel@adba99c781d3746d38b3a09b1cf797965bcb7fe6 # v4.3.0
with:
package-dir: python
output-dir: wheelhouse
env:
# The wheel is py3-none-<platform>: cibuildwheel builds it once with
# the first Python and reuses it (test only) for the others.
CIBW_BUILD: 'cp310-* cp311-* cp312-* cp313-* cp314-*'
CIBW_SKIP: '*-musllinux_*'
CIBW_ARCHS: native
CIBW_BUILD_VERBOSITY: 1

# auditwheel would tag x86_64 as old as manylinux_2_5 (the Go
# library uses only ancient glibc symbols), claiming distros that
# were never tested and that Go 1.26 (kernel 3.2+) may not run on.
# Pin the floor to manylinux_2_17 (manylinux2014) on both arches.
# cibuildwheel does not expand an {arch} placeholder here, so the
# architecture comes from the matrix (empty on macOS/Windows, where
# this Linux-only command is unused).
CIBW_REPAIR_WHEEL_COMMAND_LINUX: auditwheel repair --plat manylinux_2_17_${{ matrix.arch }} -w {dest_dir} {wheel}
CIBW_BEFORE_ALL_LINUX: bash {project}/python/scripts/install-go-linux.sh
CIBW_BEFORE_BUILD_LINUX: cd {project}/pkg/cbinding && bash build.sh
CIBW_BEFORE_BUILD_MACOS: cd {project}/pkg/cbinding && bash build.sh
CIBW_BEFORE_BUILD_WINDOWS: >-
cd /d "{project}\pkg\cbinding" &&
go build -buildmode=c-shared -o ..\..\python\pygosqlx\lib\libgosqlx.dll .

# GOFLAGS -trimpath (all platforms): keep absolute build and module
# cache paths out of the published binaries.
# -buildvcs=false: the container's copy of the checkout is owned by a
# different user, which makes Go's VCS stamping fail.
CIBW_ENVIRONMENT_LINUX: >-
PATH="/usr/local/go/bin:$PATH"
GOTOOLCHAIN=local
GOFLAGS="-buildvcs=false -trimpath"
CGO_ENABLED=1
PYGOSQLX_TEST_INSTALLED=1
# Go 1.26 targets macOS 12+, and cgo objects must not be built for a
# newer macOS than the wheel tag claims.
CIBW_ENVIRONMENT_MACOS: >-
MACOSX_DEPLOYMENT_TARGET=13.0
CGO_CFLAGS="-O2 -g -mmacosx-version-min=13.0"
CGO_LDFLAGS="-O2 -g -mmacosx-version-min=13.0"
GOTOOLCHAIN=local
GOFLAGS=-trimpath
CGO_ENABLED=1
PYGOSQLX_TEST_INSTALLED=1
CIBW_ENVIRONMENT_WINDOWS: >-
GOTOOLCHAIN=local
GOFLAGS=-trimpath
CGO_ENABLED=1
PYGOSQLX_TEST_INSTALLED=1

# Run the suite against the installed wheel, not the checkout:
# PYGOSQLX_TEST_INSTALLED=1 keeps python/ off sys.path and enables
# tests/test_installed_wheel.py, which fails if the bundled library
# is missing or the checkout was imported.
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: >-
python -m pytest --import-mode=importlib -p no:cacheprovider
{project}/python/tests -v -rs

- name: Upload wheels
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pygosqlx-wheels-${{ matrix.name }}
path: wheelhouse/*.whl
if-no-files-found: error

sdist:
name: sdist
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
# Full history so a release tag can be checked against main.
fetch-depth: 0

- name: Check Go version pins agree
run: |
script_go="$(sed -n 's/^GO_VERSION="\(.*\)"$/\1/p' python/scripts/install-go-linux.sh)"
if [ -z "$script_go" ] || [ "$script_go" != "$GO_VERSION" ]; then
echo "::error::GO_VERSION mismatch: workflow '$GO_VERSION' vs install-go-linux.sh '$script_go'"
exit 1
fi

# Defense in depth: never publish a tag whose commit is not on main.
- name: Check tagged commit is on main
if: startsWith(github.ref, 'refs/tags/pygosqlx-v')
run: |
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::$GITHUB_SHA ($GITHUB_REF_NAME) is not an ancestor of origin/main"
exit 1
fi

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.13'

- name: Check tag matches package version
if: startsWith(github.ref, 'refs/tags/pygosqlx-v')
env:
TAG: ${{ github.ref_name }}
run: |
pkg_version="$(python -c 'import tomllib; print(tomllib.load(open("python/pyproject.toml", "rb"))["project"]["version"])')"
if [ "${TAG#pygosqlx-v}" != "$pkg_version" ]; then
echo "::error::tag $TAG does not match python/pyproject.toml version $pkg_version"
exit 1
fi

- name: Build sdist
run: |
python -m pip install --upgrade build twine
python -m build --sdist --outdir dist python
python -m twine check --strict dist/*

# The sdist carries only the Python sources. It must never ship a
# prebuilt library for some other platform.
- name: Check sdist has no native binaries
run: |
if tar tzf dist/*.tar.gz | grep -E '\.(so|dylib|dll)$'; then
echo "::error::sdist contains a native library"
exit 1
fi

- name: Upload sdist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pygosqlx-sdist
path: dist/*.tar.gz
if-no-files-found: error

publish-pypi:
name: publish to PyPI
needs: [ wheels, sdist ]
# Only a tag *push* publishes to PyPI. A workflow_dispatch run on a tag
# ref also has github.ref == refs/tags/pygosqlx-v*, so the event check is
# what keeps 'target: none' / 'testpypi' dispatches off real PyPI.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/pygosqlx-v')
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
name: pypi
url: https://pypi.org/p/pygosqlx
permissions:
id-token: write # PyPI Trusted Publishing (OIDC)
steps:
- name: Download distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: pygosqlx-*
path: dist
merge-multiple: true

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

publish-testpypi:
name: publish to TestPyPI
needs: [ wheels, sdist ]
if: >-
github.event_name == 'workflow_dispatch' &&
inputs.target == 'testpypi' &&
github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
name: testpypi
url: https://test.pypi.org/p/pygosqlx
permissions:
id-token: write # TestPyPI Trusted Publishing (OIDC)
steps:
- name: Download distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: pygosqlx-*
path: dist
merge-multiple: true

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
repository-url: https://test.pypi.org/legacy/
# The version only changes per release, so repeated dry runs would
# otherwise fail on files already uploaded. TestPyPI only.
skip-existing: true
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ jobs:
CGO_ENABLED: '1'
run: task test:cbinding

# pygosqlx-v* tags (Python package releases, see pygosqlx-release.yml)
# live on main too. GoReleaser OSS derives the previous tag with
# `git describe --tags`, which would pick a pygosqlx tag and produce a
# truncated changelog and a wrong compare link. Pin both tags to the
# 'v*' series explicitly (git.ignore_tags in .goreleaser.yml is Pro-only).
- name: Resolve release tags
env:
TAG: ${{ github.ref_name }}
run: |
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "${TAG}^" 2>/dev/null || true)"
echo "GORELEASER_CURRENT_TAG=${TAG}" >> "$GITHUB_ENV"
if [ -n "$prev" ]; then
echo "GORELEASER_PREVIOUS_TAG=${prev}" >> "$GITHUB_ENV"
fi
echo "current=${TAG} previous=${prev:-<none>}"

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:
Expand Down
Loading
Loading