π Release workflow (tox-dev/filelock)
The π Release workflow from tox-dev/filelock, explained and optimized by Latchkey.
A
CI health: A - excellent
Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the π Release workflow from the tox-dev/filelock repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
workflow (.yml)
name: π Release
on:
workflow_dispatch:
inputs:
release:
description: "π Release (semver bump)?"
required: true
default: "no"
type: choice
options: ["no", patch, minor, major]
push:
branches: ["main"]
tags: ["[0-9]+.[0-9]+.[0-9]+"]
pull_request:
concurrency:
group: >-
${{ github.workflow }}-${{ github.ref }}${{
github.event.inputs.release && github.event.inputs.release != 'no' && '-release' || ''
}}
cancel-in-progress: ${{ github.event.inputs.release == 'no' || github.event.inputs.release == null }}
permissions:
contents: read
env:
dists-artifact-name: python-package-distributions
jobs:
build:
name: π¦ build
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
outputs:
version: ${{ steps.v.outputs.version }}
changelog: ${{ steps.v.outputs.changelog }}
steps:
- name: π₯ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: π¦ Setup uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false # this workflow builds the artifact that gets published; no cache to poison
- name: π Generate changelog
id: v
if: ${{ !startsWith(github.ref, 'refs/tags/') }} # on a tag push we build to publish, no changelog needed
run: >-
uv run tasks/changelog.py "$RELEASE_TYPE" "$EVENT_NUMBER" "$BASE_SHA"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TYPE:
${{ github.event.inputs.release == 'no' || github.event.inputs.release == null && 'patch' ||
github.event.inputs.release }}
EVENT_NUMBER: ${{ github.event.number }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
- name: π·οΈ Create temporary tag for hatch-vcs
if: github.event.inputs.release != 'no' && github.event.inputs.release != null
run: git tag "$STEPS_V_OUTPUTS_VERSION"
env:
STEPS_V_OUTPUTS_VERSION: ${{ steps.v.outputs.version }}
- name: π¦ Build package
run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist
- name: π€ Store the distribution packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.dists-artifact-name }}
path: dist/*
release:
name: π·οΈ tag
needs: [build]
if: github.event.inputs.release != 'no' && github.event.inputs.release != null && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
# RELEASE_TOKEN is an environment-scoped secret; without selecting the environment it resolves empty and the
# checkout fails with "Input required and not supplied: token".
environment:
name: release
permissions:
contents: write
steps:
- name: π₯ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.RELEASE_TOKEN }} # zizmor: ignore[artipacked]
- name: π€ Commit changelog and tag
env:
CHANGELOG: ${{ needs.build.outputs.changelog }}
VERSION: ${{ needs.build.outputs.version }}
run: |
if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q .; then
echo "Tag $VERSION already exists, nothing to do"
exit 0
fi
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
header="$VERSION ($(date -u +%Y-%m-%d))"
separator=$(printf '%0.s*' $(seq 1 $((${#header} + 1))))
{
head -4 docs/changelog.rst
printf '%s\n %s\n%s\n\n' "$separator" "$header" "$separator"
printf '%s\n\n' "$CHANGELOG"
tail -n +5 docs/changelog.rst
} > docs/changelog.tmp
mv docs/changelog.tmp docs/changelog.rst
git add docs/changelog.rst
git commit -m "Release $VERSION"
git tag "$VERSION"
# Push the branch, then the tag. The tag is pushed with RELEASE_TOKEN (a PAT, not GITHUB_TOKEN), so it
# triggers this same workflow on the tag ref, where the publish job below uploads to PyPI. Publishing keys
# off the tag, not this dispatch run, so any tag - including one pushed by hand - always reaches PyPI.
git push origin main
git push origin "$VERSION"
publish:
name: π publish
needs: [build]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-24.04
environment:
name: release
url: https://pypi.org/project/filelock/${{ github.ref_name }}
permissions:
id-token: write
contents: write
steps:
- name: β¬οΈ Download all the dists
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ env.dists-artifact-name }}
path: dist/
- name: π Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
skip-existing: true
- name: π’ Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
VERSION: ${{ github.ref_name }}
run: |
if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "Release $VERSION already exists, skipping"
else
gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --title="$VERSION" --verify-tag --generate-notes
fi
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: π Release on: workflow_dispatch: inputs: release: description: "π Release (semver bump)?" required: true default: "no" type: choice options: ["no", patch, minor, major] push: branches: ["main"] tags: ["[0-9]+.[0-9]+.[0-9]+"] pull_request: concurrency: group: >- ${{ github.workflow }}-${{ github.ref }}${{ github.event.inputs.release && github.event.inputs.release != 'no' && '-release' || '' }} cancel-in-progress: ${{ github.event.inputs.release == 'no' || github.event.inputs.release == null }} permissions: contents: read env: dists-artifact-name: python-package-distributions jobs: build: timeout-minutes: 30 name: π¦ build runs-on: latchkey-small permissions: contents: read pull-requests: read outputs: version: ${{ steps.v.outputs.version }} changelog: ${{ steps.v.outputs.changelog }} steps: - name: π₯ Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - name: π¦ Setup uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false # this workflow builds the artifact that gets published; no cache to poison - name: π Generate changelog id: v if: ${{ !startsWith(github.ref, 'refs/tags/') }} # on a tag push we build to publish, no changelog needed run: >- uv run tasks/changelog.py "$RELEASE_TYPE" "$EVENT_NUMBER" "$BASE_SHA" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TYPE: ${{ github.event.inputs.release == 'no' || github.event.inputs.release == null && 'patch' || github.event.inputs.release }} EVENT_NUMBER: ${{ github.event.number }} BASE_SHA: ${{ github.event.pull_request.base.sha }} - name: π·οΈ Create temporary tag for hatch-vcs if: github.event.inputs.release != 'no' && github.event.inputs.release != null run: git tag "$STEPS_V_OUTPUTS_VERSION" env: STEPS_V_OUTPUTS_VERSION: ${{ steps.v.outputs.version }} - name: π¦ Build package run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - name: π€ Store the distribution packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.dists-artifact-name }} path: dist/* release: timeout-minutes: 30 name: π·οΈ tag needs: [build] if: github.event.inputs.release != 'no' && github.event.inputs.release != null && github.ref == 'refs/heads/main' runs-on: latchkey-small # RELEASE_TOKEN is an environment-scoped secret; without selecting the environment it resolves empty and the # checkout fails with "Input required and not supplied: token". environment: name: release permissions: contents: write steps: - name: π₯ Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: token: ${{ secrets.RELEASE_TOKEN }} # zizmor: ignore[artipacked] - name: π€ Commit changelog and tag env: CHANGELOG: ${{ needs.build.outputs.changelog }} VERSION: ${{ needs.build.outputs.version }} run: | if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q .; then echo "Tag $VERSION already exists, nothing to do" exit 0 fi git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" header="$VERSION ($(date -u +%Y-%m-%d))" separator=$(printf '%0.s*' $(seq 1 $((${#header} + 1)))) { head -4 docs/changelog.rst printf '%s\n %s\n%s\n\n' "$separator" "$header" "$separator" printf '%s\n\n' "$CHANGELOG" tail -n +5 docs/changelog.rst } > docs/changelog.tmp mv docs/changelog.tmp docs/changelog.rst git add docs/changelog.rst git commit -m "Release $VERSION" git tag "$VERSION" # Push the branch, then the tag. The tag is pushed with RELEASE_TOKEN (a PAT, not GITHUB_TOKEN), so it # triggers this same workflow on the tag ref, where the publish job below uploads to PyPI. Publishing keys # off the tag, not this dispatch run, so any tag - including one pushed by hand - always reaches PyPI. git push origin main git push origin "$VERSION" publish: timeout-minutes: 30 name: π publish needs: [build] if: startsWith(github.ref, 'refs/tags/') runs-on: latchkey-small environment: name: release url: https://pypi.org/project/filelock/${{ github.ref_name }} permissions: id-token: write contents: write steps: - name: β¬οΈ Download all the dists uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: π Publish to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true - name: π’ Create GitHub release env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} VERSION: ${{ github.ref_name }} run: | if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then echo "Release $VERSION already exists, skipping" else gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --title="$VERSION" --verify-tag --generate-notes fi
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.