Skip to content
Merged
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
109 changes: 107 additions & 2 deletions .github/workflows/build.python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ on:
required: false
type: string
default: "ubuntu-latest"
uv-workspace:
required: false
type: boolean
default: false
description: >-
Set to true if `working-directory` is a member of a uv workspace
(i.e. it has local path dependencies on sibling packages declared via
`tool.uv.sources` with `workspace = true`, such as other packages
under a monorepo's `libs/` directory). When enabled, all workspace
members are built together and the package's local (workspace)
dependencies are installed from the resulting wheels instead of
being resolved from a package index. This avoids failures for
private, unpublished packages, and avoids name collisions with
unrelated public packages that happen to share the same name.
Requires the uv workspace root (the `pyproject.toml`/`uv.lock`
declaring `[tool.uv.workspace]`) to be at the repository root.
python-platform:
required: false
type: string
default: ""
description: >-
Optional target platform to install dependencies for, passed to
`uv pip install --python-platform`. Useful when the build runner's
architecture differs from the deployment target's, e.g.
"aarch64-manylinux_2_28" when building on an x86_64 runner for an
arm64 Lambda.

outputs:
artifact-name:
Expand All @@ -47,19 +73,98 @@ jobs:
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "${{ inputs.working-directory }}/uv.lock"
cache-dependency-glob: "${{ inputs.uv-workspace && 'uv.lock' || format('{0}/uv.lock', inputs.working-directory) }}"
Comment thread
isabelle-galleberg marked this conversation as resolved.

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ inputs.python_version }}

- name: Install dependencies
if: ${{ !inputs.uv-workspace }}
working-directory: ${{ inputs.working-directory }}
run: |
uv build
uv pip install dist/*.whl --target ./out
uv pip install dist/*.whl --target ./out ${{ inputs.python-platform != '' && format('--python-platform {0}', inputs.python-platform) || '' }}
find ./out/ -name "__pycache__" -type d -exec rm -rf {} \;

- name: Install dependencies (uv workspace)
if: ${{ inputs.uv-workspace }}
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
PYTHON_PLATFORM: ${{ inputs.python-platform }}
run: |
set -euo pipefail

if ! python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)"; then
echo "uv-workspace: true requires python_version >= 3.11 (tomllib is stdlib only from 3.11)." >&2
exit 1
fi

# `uv sync`/`uv build --all-packages` operate on the uv workspace
# found from the current directory upwards, and we always run them
# from the repo root (see below) - so the workspace root must be
# the repo root itself, not some nested directory.
if [ ! -f "uv.lock" ] || ! grep -q '^\[tool\.uv\.workspace\]' pyproject.toml 2>/dev/null; then
echo "uv-workspace: true requires the uv workspace root (pyproject.toml with [tool.uv.workspace], and uv.lock) to be at the repository root." >&2
exit 1
fi

# `uv build`/`uv export` for a workspace member resolve the workspace
# root themselves, but write relative output (e.g. `dist/`) relative
# to the current directory, so we always run from the repo root to
# get a single, predictable location for every workspace member's
# wheel.
PKG_NAME="$(python3 -c "import tomllib; print(tomllib.load(open('${WORKING_DIRECTORY}/pyproject.toml','rb'))['project']['name'])")"

# Creates the uv environment without updating or changing uv.lock
# Build every workspace member once, so local (path) dependencies of
# this package are available as real wheels alongside it.
uv sync --frozen
uv build --all-packages --wheel

# Pin every *third-party* dependency to the exact resolved version
# from the lockfile. Workspace members are intentionally excluded
# here (`--no-emit-workspace`) - they are installed explicitly from
# local wheels below instead, since they aren't published to any
# index and their names could otherwise collide with unrelated
# public packages.
uv export --frozen --package "$PKG_NAME" --no-emit-workspace --no-dev --no-hashes -o /tmp/uv-workspace-constraints.txt

# Discover which sibling workspace packages this package actually
# depends on (directly or transitively), so we install exactly the
# matching local wheels and nothing else. Note: `uv export` also
# emits an `-e <path>` entry for $PKG_NAME itself (not just its
# workspace dependencies), so the loop below picks up and installs
# its own freshly-built wheel too - there's no separate, explicit
# install of $PKG_NAME's wheel anywhere in this script.
uv export --frozen --package "$PKG_NAME" --no-dev --no-hashes -o /tmp/uv-workspace-full-export.txt

LOCAL_WHEELS=()
while IFS= read -r local_dir; do
[ -z "$local_dir" ] && continue
local_name="$(python3 -c "import tomllib; print(tomllib.load(open('${local_dir}/pyproject.toml','rb'))['project']['name'])")"
Comment thread
SondreODahl marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.
# Wheel filenames use the normalized distribution name: lowercased,
# with runs of `-`, `_` and `.` collapsed to a single `_`.
normalized_name="$(echo "$local_name" | tr '[:upper:]' '[:lower:]' | sed -E 's/[-_.]+/_/g')"
wheel="$(find dist -maxdepth 1 -type f -name "${normalized_name}-*.whl" -print -quit)"
if [ -z "$wheel" ]; then
echo "No wheel found for workspace package ${local_name}" >&2
exit 1
fi
LOCAL_WHEELS+=("$wheel")
done < <(awk '$1 == "-e" { print $2 }' /tmp/uv-workspace-full-export.txt)

mkdir -p "${WORKING_DIRECTORY}/out"

INSTALL_ARGS=(-c /tmp/uv-workspace-constraints.txt --target "${WORKING_DIRECTORY}/out")
if [ -n "$PYTHON_PLATFORM" ]; then
INSTALL_ARGS+=(--python-platform "$PYTHON_PLATFORM")
fi

uv pip install "${INSTALL_ARGS[@]}" "${LOCAL_WHEELS[@]}"

find "${WORKING_DIRECTORY}/out" -name "__pycache__" -type d -exec rm -rf {} \;

- name: Validate and build artifact name
id: artifact-name
run: |
Expand Down