Skip to content

feat: support Python 3.15 on Linux and Windows #33

feat: support Python 3.15 on Linux and Windows

feat: support Python 3.15 on Linux and Windows #33

Workflow file for this run

name: Release
on:
push:
branches:
- main
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g. 1.0.0)"
required: true
type: string
dry_run:
description: "Validate and build without creating a tag or release"
required: false
type: boolean
default: true
concurrency:
group: release-${{ github.event_name == 'push' && github.ref_name || inputs.version }}
cancel-in-progress: false
permissions:
contents: read
jobs:
prepare:
name: Prepare release
if: >-
github.event_name != 'push' ||
github.ref_type == 'tag' ||
startsWith(github.event.head_commit.message, 'chore: release v')
runs-on: ubuntu-24.04
permissions:
contents: write
outputs:
version: ${{ steps.meta.outputs.version }}
build: ${{ steps.meta.outputs.build }}
publish: ${{ steps.meta.outputs.publish }}
ref: ${{ steps.meta.outputs.ref }}
tag_needed: ${{ steps.meta.outputs.tag_needed }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Resolve release mode
id: meta
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
run: |
TAG_NEEDED=false
if [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF_TYPE" == "tag" ]]; then
VERSION="${GITHUB_REF_NAME#v}"
BUILD=true
PUBLISH=true
REF="$GITHUB_REF_NAME"
elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]]; then
SUBJECT="$(git log -1 --pretty=%s)"
if [[ "$SUBJECT" =~ ^chore:\ release\ v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
VERSION="${BASH_REMATCH[1]}"
else
echo "::error::Main-branch release commits must be named 'chore: release vX.Y.Z'"
exit 1
fi
BUILD=true
PUBLISH=true
REF="$GITHUB_SHA"
TAG_NEEDED=true
else
VERSION="$INPUT_VERSION"
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
BUILD=true
PUBLISH=false
REF="$GITHUB_SHA"
else
BUILD=true
PUBLISH=true
REF="v$VERSION"
fi
fi
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version: $VERSION (expected X.Y.Z)"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "build=$BUILD" >> "$GITHUB_OUTPUT"
echo "publish=$PUBLISH" >> "$GITHUB_OUTPUT"
echo "ref=$REF" >> "$GITHUB_OUTPUT"
echo "tag_needed=$TAG_NEEDED" >> "$GITHUB_OUTPUT"
- name: Validate tag-triggered changelog
if: github.event_name == 'push'
shell: bash
run: |
VERSION="${{ steps.meta.outputs.version }}"
grep -F "## [$VERSION]" CHANGELOG.md
- name: Tag prepared release commit
if: steps.meta.outputs.tag_needed == 'true'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
VERSION="${{ steps.meta.outputs.version }}"
if gh release view "v$VERSION" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "::error::Release v$VERSION already exists"
exit 1
fi
if git rev-parse --verify --quiet "refs/tags/v$VERSION"; then
CURRENT_TAG="$(git rev-list -n 1 "v$VERSION")"
if [[ "$CURRENT_TAG" != "$GITHUB_SHA" ]]; then
echo "::error::Refusing to move existing release tag v$VERSION from $CURRENT_TAG"
exit 1
fi
else
git tag "v$VERSION" "$GITHUB_SHA"
git push origin "refs/tags/v$VERSION"
fi
- name: Preview unreleased notes
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
shell: bash
run: |
python - <<'PY'
from pathlib import Path
text = Path("CHANGELOG.md").read_text()
start = text.index("## [Unreleased]") + len("## [Unreleased]")
end = text.find("\n## [", start)
notes = text[start:] if end < 0 else text[start:end]
notes = notes.strip()
if not notes:
raise SystemExit("Unreleased changelog is empty")
print(notes)
PY
- name: Create release commit and tag
if: github.event_name == 'workflow_dispatch' && !inputs.dry_run
shell: bash
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "::error::Non-dry-run releases must be started from main"
exit 1
fi
VERSION="${{ steps.meta.outputs.version }}"
if git rev-parse --verify --quiet "refs/tags/v$VERSION"; then
echo "::error::Tag v$VERSION already exists"
exit 1
fi
DATE=$(date -u +%Y-%m-%d)
VERSION="$VERSION" DATE="$DATE" python - <<'PY'
import os
from pathlib import Path
path = Path("CHANGELOG.md")
text = path.read_text()
marker = "## [Unreleased]"
start = text.index(marker) + len(marker)
next_heading = text.find("\n## [", start)
notes = text[start:] if next_heading < 0 else text[start:next_heading]
if not notes.strip():
raise SystemExit("Unreleased changelog is empty")
version = os.environ["VERSION"]
date = os.environ["DATE"]
text = text.replace(marker, f"{marker}\n\n## [{version}] - {date}", 1)
path.write_text(text)
PY
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "chore: release v$VERSION"
git tag "v$VERSION"
git push --atomic origin HEAD:main "refs/tags/v$VERSION"
- name: Release handoff
if: github.event_name == 'workflow_dispatch' && !inputs.dry_run
run: echo "Release commit and tag pushed; this workflow run will build and publish the assets."
package:
name: Test & package
if: needs.prepare.outputs.build == 'true'
needs: prepare
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.ref }}
fetch-depth: 0
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14"
enable-cache: true
- name: Install
run: |
uv venv .venv --python 3.14
.venv/bin/python -m ensurepip --upgrade
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -e ".[dev]"
- name: Lint
run: .venv/bin/ruff check src tests tools
- name: Check formatting
run: .venv/bin/ruff format --check src tests tools
- name: Test
run: .venv/bin/python -m pytest
- name: Build assets
run: |
.venv/bin/python -m build
.venv/bin/python tools/build_offline.py --output dist/endkeep-offline.py
cp requirements-offline.txt dist/requirements-offline.txt
(
cd dist
sha256sum *.whl endkeep-offline.py requirements-offline.txt > SHA256SUMS
)
- name: Wheel metadata smoke
run: |
uv venv /tmp/endkeep-wheel-smoke --python 3.14
/tmp/endkeep-wheel-smoke/bin/python -m ensurepip --upgrade
/tmp/endkeep-wheel-smoke/bin/python -m pip install --no-deps dist/*.whl
/tmp/endkeep-wheel-smoke/bin/python - <<'PY'
from importlib.metadata import metadata
package = metadata("endstone-endkeep")
assert package["Name"] == "endstone-endkeep"
print(package["Name"], package["Version"])
PY
- name: Standalone repository-only restore
run: |
.venv/bin/python tests/standalone_fixture.py /tmp/endkeep-fixture
uv venv /tmp/endkeep-offline-smoke --python 3.14
/tmp/endkeep-offline-smoke/bin/python -m ensurepip --upgrade
/tmp/endkeep-offline-smoke/bin/python -m pip install -r requirements-offline.txt
/tmp/endkeep-offline-smoke/bin/python dist/endkeep-offline.py --repo /tmp/endkeep-fixture/repo list
/tmp/endkeep-offline-smoke/bin/python dist/endkeep-offline.py --repo /tmp/endkeep-fixture/repo verify
/tmp/endkeep-offline-smoke/bin/python dist/endkeep-offline.py --repo /tmp/endkeep-fixture/repo restore /tmp/endkeep-restored
test -f /tmp/endkeep-restored/level.dat
test -d /tmp/endkeep-restored/db
- name: Upload release-shaped artifact
uses: actions/upload-artifact@v7
with:
name: endkeep-release-${{ needs.prepare.outputs.version }}
path: |
dist/*.whl
dist/endkeep-offline.py
dist/requirements-offline.txt
dist/SHA256SUMS
if-no-files-found: error
retention-days: 14
publish:
name: Publish GitHub release
if: needs.prepare.outputs.publish == 'true'
needs: [prepare, package]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Checkout tag
uses: actions/checkout@v7
with:
ref: v${{ needs.prepare.outputs.version }}
fetch-depth: 0
- name: Download assets
uses: actions/download-artifact@v8
with:
name: endkeep-release-${{ needs.prepare.outputs.version }}
path: assets
- name: Extract release notes
shell: bash
run: |
VERSION="${{ needs.prepare.outputs.version }}" python - <<'PY'
import os
from pathlib import Path
version = os.environ["VERSION"]
text = Path("CHANGELOG.md").read_text()
marker = f"## [{version}]"
start = text.index(marker)
start = text.index("\n", start) + 1
end = text.find("\n## [", start)
notes = text[start:] if end < 0 else text[start:end]
notes = notes.strip()
if not notes:
raise SystemExit(f"No changelog notes for {version}")
Path("/tmp/release-notes.md").write_text(notes + "\n")
PY
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
VERSION="${{ needs.prepare.outputs.version }}"
gh release create "v$VERSION" \
--repo "${{ github.repository }}" \
--verify-tag \
--title "v$VERSION" \
--notes-file /tmp/release-notes.md \
assets/*.whl \
assets/endkeep-offline.py \
assets/requirements-offline.txt \
assets/SHA256SUMS