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
70 changes: 57 additions & 13 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,19 @@ jobs:
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}

publish-pypi-package:
name: Build and publish Python package to PyPI
runs-on: ubuntu-latest
validate-release:
name: Validate released commit
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
environment:
name: pypi
url: https://pypi.org/p/copick-utils
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
uses: ./.github/workflows/test.yml
with:
ref: ${{ needs.release-please.outputs.sha }}

build-release:
name: Build and inspect release artifacts
runs-on: ubuntu-latest
needs: [release-please, validate-release]
if: needs.release-please.outputs.release_created == 'true'
steps:
- name: Checkout released commit
uses: actions/checkout@v7
Expand All @@ -69,14 +72,55 @@ jobs:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
with:
version: "0.7.13"
version: "0.12.4"
python-version: "3.12"

- name: build
run: |
uv build
- name: Check lockfile
run: uv lock --check

- name: Install inspection environment
run: uv sync --locked --extra test

- name: Verify Python version
run: >
uv run --no-sync python -c
"import sys; v = sys.version_info;
assert (v.major, v.minor) == (3, 12), sys.version;
assert v.releaselevel == 'final', sys.version;
print(sys.version)"

- name: Build distributions
run: uv build

- name: Inspect distributions
run: uv run --no-sync python scripts/inspect_distribution.py dist

- name: Preserve validated artifacts
uses: actions/upload-artifact@v7
with:
name: copick-utils-${{ needs.release-please.outputs.sha }}
path: dist/
if-no-files-found: error
retention-days: 7

publish-pypi-package:
name: Publish validated Python package to PyPI
runs-on: ubuntu-latest
needs: [release-please, build-release]
if: needs.release-please.outputs.release_created == 'true'
environment:
name: pypi
url: https://pypi.org/p/copick-utils
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- name: Download validated artifacts
uses: actions/download-artifact@v7
with:
name: copick-utils-${{ needs.release-please.outputs.sha }}
path: dist/

- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Tests

on:
workflow_call:
inputs:
ref:
description: Exact commit to validate
required: false
type: string
push:
branches: [main, v2.0]
paths-ignore:
Expand All @@ -24,6 +30,8 @@ jobs:

steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}

- name: Install uv
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
Expand Down
49 changes: 49 additions & 0 deletions scripts/inspect_distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Fail release builds whose artifacts do not expose the migrated contract."""

import argparse
import tarfile
import zipfile
from pathlib import Path

EXPECTED_ENTRY_POINTS = 32


def inspect_distributions(dist_dir: Path) -> tuple[Path, Path]:
wheels = list(dist_dir.glob("*.whl"))
sdists = list(dist_dir.glob("*.tar.gz"))
if len(wheels) != 1 or len(sdists) != 1:
raise ValueError(f"Expected one wheel and one source distribution, found {wheels!r} and {sdists!r}")

wheel = wheels[0]
with zipfile.ZipFile(wheel) as archive:
metadata_names = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
entry_point_names = [name for name in archive.namelist() if name.endswith(".dist-info/entry_points.txt")]
if len(metadata_names) != 1 or len(entry_point_names) != 1:
raise ValueError("Wheel must contain exactly one METADATA and one entry_points.txt file")

entry_points = archive.read(entry_point_names[0]).decode()
command_count = sum(
1 for line in entry_points.splitlines() if line and not line.startswith("[") and "=" in line
)
if command_count != EXPECTED_ENTRY_POINTS:
raise ValueError(f"Expected {EXPECTED_ENTRY_POINTS} command entry points, found {command_count}")

sdist = sdists[0]
with tarfile.open(sdist, "r:gz") as archive:
names = archive.getnames()
if not any(name.endswith("/uv.lock") for name in names):
raise ValueError("Source distribution does not contain uv.lock")

return wheel, sdist


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("dist_dir", type=Path)
args = parser.parse_args()
wheel, sdist = inspect_distributions(args.dist_dir)
print(f"Validated {wheel.name} and {sdist.name}")


if __name__ == "__main__":
main()
Loading