Skip to content
Latchkey

πŸš€ 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.

Grade your own workflow free or run it on Latchkey →
Source: tox-dev/filelock.github/workflows/release.yamlLicense MITView source

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

This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow