Release workflow (powerapi-ng/powerapi)
The Release workflow from powerapi-ng/powerapi, explained and optimized by Latchkey.
CI health: D - needs work
Point runs-on at Latchkey and get caching, run de-duplication, 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 powerapi-ng/powerapi repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-3-Clause license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
pre-checks:
name: Release Pre-Checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check if package version corresponds to git tag
shell: python
env:
PYTHONPATH: ${{ github.workspace }}/src
run: |
import os
import sys
from powerapi import __version__
git_tag = os.environ['GITHUB_REF_NAME'].removeprefix('v')
pkg_version = __version__
if git_tag != pkg_version:
title = 'Invalid version'
file = 'src/powerapi/__init__.py'
msg = f'Version mismatch between python package ({pkg_version}) and git tag ({git_tag})'
print(f'::error title={title},file={file}::{msg}')
sys.exit(1)
build-python-package:
name: Build Python Package
runs-on: ubuntu-latest
needs: [pre-checks]
outputs:
dist-hashes: ${{ steps.dist-hashes.outputs.hash }}
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
- name: Build sdist and wheel
run: |
uv build --sdist --wheel --out-dir dist/
- name: Compute SHA256 hashes of build artifacts
id: dist-hashes
shell: bash
run: |
cd ./dist && echo "hash=$(sha256sum -- * | base64 -w0)" >> $GITHUB_OUTPUT
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "python-build-dist"
path: ./dist
if-no-files-found: error
publish-python-package:
name: Publish Pypi Package
runs-on: ubuntu-latest
needs: [build-python-package]
permissions:
contents: read
id-token: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "python-build-dist"
path: dist
- name: Publish package
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
print-hash: true
attestations: true
docker-image:
name: Publish Docker image
runs-on: ubuntu-latest
needs: [pre-checks, publish-python-package]
permissions:
contents: read
packages: write
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ vars.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
docker.io/powerapi/powerapi
ghcr.io/powerapi-ng/powerapi
tags: |
type=pep440,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
id: build-and-push
with:
push: true
provenance: false
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
POWERAPI_INSTALL_METHOD=pypi
POWERAPI_VERSION=${{ steps.meta.outputs.version }}
generate-release-notes:
name: Generate release notes
runs-on: ubuntu-latest
needs: [pre-checks]
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Generate version changelog
uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0
with:
config: .cliff.toml
args: --no-exec --latest
env:
OUTPUT: CHANGELOG.md
GITHUB_REPO: ${{ github.repository }}
- name: Upload release notes artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "release-notes"
path: CHANGELOG.md
if-no-files-found: error
github-release:
name: Publish GitHub release
runs-on: ubuntu-latest
needs: [publish-python-package, docker-image, generate-release-notes]
permissions:
contents: write
steps:
- name: Download release notes artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "release-notes"
- name: Create GitHub release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
body_path: CHANGELOG.md
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
name: Release on: push: tags: - "v*" permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: pre-checks: timeout-minutes: 30 name: Release Pre-Checks runs-on: latchkey-small permissions: contents: read steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check if package version corresponds to git tag shell: python env: PYTHONPATH: ${{ github.workspace }}/src run: | import os import sys from powerapi import __version__ git_tag = os.environ['GITHUB_REF_NAME'].removeprefix('v') pkg_version = __version__ if git_tag != pkg_version: title = 'Invalid version' file = 'src/powerapi/__init__.py' msg = f'Version mismatch between python package ({pkg_version}) and git tag ({git_tag})' print(f'::error title={title},file={file}::{msg}') sys.exit(1) build-python-package: timeout-minutes: 30 name: Build Python Package runs-on: latchkey-small needs: [pre-checks] outputs: dist-hashes: ${{ steps.dist-hashes.outputs.hash }} permissions: contents: read steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: cache: 'pip' python-version: "3.x" - name: Install uv uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0 - name: Build sdist and wheel run: | uv build --sdist --wheel --out-dir dist/ - name: Compute SHA256 hashes of build artifacts id: dist-hashes shell: bash run: | cd ./dist && echo "hash=$(sha256sum -- * | base64 -w0)" >> $GITHUB_OUTPUT - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: "python-build-dist" path: ./dist if-no-files-found: error publish-python-package: timeout-minutes: 30 name: Publish Pypi Package runs-on: latchkey-small needs: [build-python-package] permissions: contents: read id-token: write steps: - name: Download build artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "python-build-dist" path: dist - name: Publish package uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: print-hash: true attestations: true docker-image: timeout-minutes: 30 name: Publish Docker image runs-on: latchkey-small needs: [pre-checks, publish-python-package] permissions: contents: read packages: write steps: - name: Set up QEMU uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Setup Docker buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log in to Docker Hub uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ vars.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Log in to GitHub Container Registry uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | docker.io/powerapi/powerapi ghcr.io/powerapi-ng/powerapi tags: | type=pep440,pattern={{version}} - name: Build and push Docker image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 id: build-and-push with: push: true provenance: false platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | POWERAPI_INSTALL_METHOD=pypi POWERAPI_VERSION=${{ steps.meta.outputs.version }} generate-release-notes: timeout-minutes: 30 name: Generate release notes runs-on: latchkey-small needs: [pre-checks] permissions: contents: read steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Generate version changelog uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 with: config: .cliff.toml args: --no-exec --latest env: OUTPUT: CHANGELOG.md GITHUB_REPO: ${{ github.repository }} - name: Upload release notes artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: "release-notes" path: CHANGELOG.md if-no-files-found: error github-release: timeout-minutes: 30 name: Publish GitHub release runs-on: latchkey-small needs: [publish-python-package, docker-image, generate-release-notes] permissions: contents: write steps: - name: Download release notes artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "release-notes" - name: Create GitHub release uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: body_path: CHANGELOG.md
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. - Cancel superseded runs when a branch or PR gets a newer push.
- Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Container pulls and builds
This workflow runs 6 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.