name: Release on: push: tags: ["v*"] jobs: release: runs-on: fedora steps: - uses: actions/checkout@v4 - name: Set up Python and uv uses: astral-sh/setup-uv@v5 with: python-version: "3.14" enable-cache: true cache-dependency-glob: "uv.lock" - name: Resolve and verify version id: version env: REF: ${{ gitea.ref }} run: | set -euo pipefail VERSION="${REF##*/}" if [[ ! "$VERSION" =~ ^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$ ]]; then echo "ERROR: expected a semver tag like v1.2.3, got: '$VERSION' (ref: '$REF')" exit 1 fi VERSION_NO_V="${VERSION#v}" PYPROJECT_VERSION="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" if [ "$VERSION_NO_V" != "$PYPROJECT_VERSION" ]; then echo "ERROR: tag ${VERSION} does not match pyproject.toml version (${PYPROJECT_VERSION})" exit 1 fi echo "version_no_v=${VERSION_NO_V}" >> "$GITEA_OUTPUT" - name: Quality gate run: | set -euo pipefail uv sync --frozen --group dev uv run ruff check src/ tests/ uv run ruff format --check src/ tests/ uv run pytest - name: Build wheel and sdist env: VERSION_NO_V: ${{ steps.version.outputs.version_no_v }} run: | set -euo pipefail uv pip install --upgrade build uv run python -m build ls -lh "dist/volumen-${VERSION_NO_V}"* - name: Extract CHANGELOG section env: VERSION_NO_V: ${{ steps.version.outputs.version_no_v }} run: | set -euo pipefail sed -n "/^## \[${VERSION_NO_V}\] /,/^## \[/p" CHANGELOG.md \ | sed '$d' \ | tail -n +2 \ > release-body.md if [ ! -s release-body.md ]; then echo "ERROR: no CHANGELOG section found for ${VERSION_NO_V}" echo "Expected a heading like: ## [${VERSION_NO_V}] — YYYY-MM-DD" exit 1 fi echo "Release body preview:" head -n 20 release-body.md - name: Create release id: create env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} run: | set -euo pipefail BODY="$(python -c 'import json,sys; print(json.dumps(sys.stdin.read()))' < release-body.md)" response="$(curl -sS -w '\n%{http_code}' \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -X POST \ "${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \ -d "{ \"tag_name\": \"$GITHUB_REF_NAME\", \"name\": \"$GITHUB_REF_NAME\", \"body\": ${BODY}, \"draft\": false, \"prerelease\": false }")" http_code="$(echo "$response" | tail -1)" payload="$(echo "$response" | sed '$d')" echo "HTTP ${http_code}" if [ "$http_code" != "201" ]; then echo "Failed to create release: ${payload}" exit 1 fi RELEASE_ID="$(echo "$payload" | python -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])')" echo "Created release ID=${RELEASE_ID}" echo "release_id=${RELEASE_ID}" >> "$GITEA_OUTPUT" - name: Upload wheel and sdist assets env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} VERSION_NO_V: ${{ steps.version.outputs.version_no_v }} RELEASE_ID: ${{ steps.create.outputs.release_id }} GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} run: | set -euo pipefail for ASSET in "volumen-${VERSION_NO_V}-py3-none-any.whl" \ "volumen-${VERSION_NO_V}.tar.gz"; do if [ ! -f "dist/${ASSET}" ]; then echo "Missing build artefact: dist/${ASSET}" exit 1 fi http_code="$(curl -sS -o /dev/null -w '%{http_code}' \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/octet-stream" \ -X POST \ --data-binary "@dist/${ASSET}" \ "${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${ASSET}")" echo "Uploaded ${ASSET}: HTTP ${http_code}" if [ "$http_code" != "201" ]; then echo "Failed to upload ${ASSET}" exit 1 fi done echo "Release $GITHUB_REF_NAME is live with wheel + sdist." # Publish to PyPI (only on tag push). Requires `PYPI_TOKEN` in repo # secrets (project-scoped upload token from https://pypi.org/manage/account/publishing/). - name: Publish to PyPI if: startsWith(env.GITEA_REF_NAME, 'v') run: | uv publish --token "$PYPI_TOKEN"