1- # .github/workflows/release-python .yml
1+ # .github/workflows/release.yml
22name : Release (PyPI + versioned docs)
3+
34# Designed to run only on version tags (e.g., vx.y.z).
45# Final step before distribution.
56# It builds distribution files, runs tests, prepares release, and publishes.
67# IMPORTANT: The PyPI project must enable Trusted Publishers and recognize this repository.
78
9+ # CI PHASES A-B-C-D:
10+ # - Assemble: Install dependencies, verify environment setup
11+ # - Baseline: Core validation (types exist, lint passes, tests pass)
12+ # - Coverage: Generate coverage / testing reports
13+ # - Deploy: Build package and docs (sanity checks for release readiness)
14+
815on :
916 push :
1017 tags :
11- - " v*.*.*" # trigger only on version tags like vx.y.z
18+ - " v*.*.*"
1219
1320permissions :
14- contents : write # create the GitHub Release
15- id-token : write # PyPI Trusted Publishing (OIDC)
21+ contents : write # create the GitHub Release
22+ id-token : write # PyPI Trusted Publishing (OIDC)
1623
17- concurrency :
18- group : release-${{ github.ref }}
19- cancel-in-progress : true
24+ env :
25+ PYTHONUNBUFFERED : " 1" # real-time logging
26+ PYTHONIOENCODING : " utf-8"
27+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
2028
2129jobs :
22- release :
23- uses : civic-interconnect/.github/.github/workflows/release-python.yml@main
30+ build-and-release :
31+ runs-on : ubuntu-latest
32+ environment : pypi
33+ timeout-minutes : 30
34+
35+ steps :
36+ # ------------------- ASSEMBLE -------------------
37+
38+ - name : A1) Checkout (with tag history)
39+ uses : actions/checkout@v5
40+ with :
41+ fetch-depth : 0 # Full history for tags
42+
43+ - name : A2) Install uv (with caching)
44+ uses : astral-sh/setup-uv@v7
45+ with :
46+ enable-cache : true
47+
48+ - name : A3) Pin Python version for consistency
49+ run : uv python pin 3.12
50+
51+ - name : A4) Display version information
52+ run : |
53+ python --version
54+ uv --version
55+ uv pip list | head -n 50
56+
57+ - name : A5) Sync to install dependencies
58+ run : |
59+ uv sync --extra dev --extra docs --upgrade
60+
61+ # ------------------- BASELINE / TESTS -------------------
62+
63+ - name : B1) Validate pyproject
64+ run : uvx validate-pyproject
65+
66+ - name : B2) Ruff lint (check only)
67+ run : uvx ruff check .
68+
69+ - name : B3) Pyright (type check)
70+ run : uv run pyright
71+
72+ - name : B4) Deptry (dependency check)
73+ run : uvx deptry .
74+
75+ # ------------------- COVERAGE (TESTING) -------------------
76+
77+ - name : C1) Run tests (pytest; coverage per pytest.ini)
78+ run : uv run pytest -q
79+
80+ # ------------ DEPLOY (BUILD, RELEASE, DOCS) -------------------
81+
82+ - name : D1) Extract plain version from tag (no leading 'v')
83+ id : ver
84+ shell : bash
85+ run : echo "plain=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
86+
87+ - name : D2) Build sdist + wheel
88+ env :
89+ SETUPTOOLS_SCM_PRETEND_VERSION : ${{ steps.ver.outputs.plain }}
90+ run : uv build
91+
92+ - name : D3) Ensure built artifact version matches tag
93+ shell : bash
94+ run : |
95+ set -e
96+ TAG="${{ steps.ver.outputs.plain }}"
97+ echo "Tag version: $TAG"
98+ ls dist
99+ # Check that at least one artifact embeds the same version
100+ if ! ls dist/*"$TAG"*.whl dist/*"$TAG"*.tar.gz >/dev/null 2>&1; then
101+ echo "ERROR: Built artifact version does not match tag $TAG"
102+ exit 1
103+ fi
104+ echo "Artifact version matches tag."
105+
106+ - name : D4) Validate built artifacts
107+ run : uv run twine check dist/*
108+
109+ - name : D5) List dist artifacts
110+ run : ls -lah dist
111+
112+ - name : D6) Create GitHub Release and upload artifacts
113+ uses : softprops/action-gh-release@v2
114+ with :
115+ tag_name : ${{ github.ref_name }} # e.g., vx.y.z
116+ name : ${{ github.ref_name }}
117+ generate_release_notes : true
118+ files : |
119+ dist/*.whl
120+ dist/*.tar.gz
121+ draft : false
122+ prerelease : false
123+ make_latest : true
124+
125+ - name : D7) Publish to PyPI
126+ uses : pypa/gh-action-pypi-publish@release/v1
127+
128+ - name : D8) Ensure 'latest' is not a version (self-heal)
129+ if : hashFiles('mkdocs.yml', 'mkdocs.yaml') != ''
130+ run : uv run mike delete latest --push || true
131+
132+ - name : D9) Deploy docs with Mike for this version and set latest
133+ if : hashFiles('mkdocs.yml', 'mkdocs.yaml') != ''
134+ run : |
135+ set -e
136+ git config user.name "github-actions[bot]"
137+ git config user.email "github-actions[bot]@users.noreply.github.com"
138+ VERSION="${{ steps.ver.outputs.plain }}"
139+ echo "Deploying docs for version: $VERSION"
140+ uv run mike deploy --push --update-aliases "$VERSION" latest
141+ uv run mike set-default --push latest
142+
143+ - name : D10) Show mike versions (after)
144+ if : hashFiles('mkdocs.yml', 'mkdocs.yaml') != ''
145+ run : uv run mike list
0 commit comments