Auto Release workflow (mjcumming/wiim)
The Auto Release workflow from mjcumming/wiim, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Auto Release workflow from the mjcumming/wiim 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
# Enable automatic release workflow
name: Auto Release
on:
push:
branches:
- main
paths:
- "custom_components/wiim/manifest.json"
workflow_dispatch:
inputs:
reason:
description: "Manual trigger reason"
required: false
type: string
jobs:
check-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version-changed: ${{ steps.check.outputs.changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Get current version
id: version
run: |
VERSION=$(grep '"version"' custom_components/wiim/manifest.json | sed 's/.*"version": "\(.*\)".*/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Current version: $VERSION"
- name: Get previous version
id: prev-version
run: |
git checkout HEAD~1
PREV_VERSION=$(grep '"version"' custom_components/wiim/manifest.json | sed 's/.*"version": "\(.*\)".*/\1/' || echo "none")
echo "prev-version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "Previous version: $PREV_VERSION"
git checkout -
- name: Check if version changed
id: check
run: |
if [ "${{ steps.version.outputs.version }}" != "${{ steps.prev-version.outputs.prev-version }}" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "Version changed from ${{ steps.prev-version.outputs.prev-version }} to ${{ steps.version.outputs.version }}"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "Version unchanged"
fi
# HACS validation - only needed for releases
# Note: Tests and linting are handled by the CI workflow, which runs on every push.
# This workflow only runs when manifest.json changes, so CI will have already validated the code.
hacs-validation:
needs: check-version
if: needs.check-version.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
# --- HACS repository validation ---------------------------------------------------
# Validate that the repository follows all rules required by HACS.
# This uses the official HACS Action (https://github.com/hacs/action)
# so that every push and every automated release is guaranteed to be
# accepted by HACS without manual re-validation.
- name: Validate repository with HACS Action
uses: hacs/action@22.5.0
with:
category: "integration"
ignore: "topics brands"
release:
needs: [check-version, hacs-validation]
if: needs.check-version.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Wait for CI tests to complete
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "β³ Waiting for CI tests to complete before creating release..."
echo "This ensures the release is only created after all tests pass."
# Get the commit SHA that triggered this workflow
COMMIT_SHA="${{ github.sha }}"
WORKFLOW_NAME="CI"
REPO="${{ github.repository }}"
# Wait up to 15 minutes for CI to complete
MAX_WAIT=900
ELAPSED=0
INTERVAL=10
while [ $ELAPSED -lt $MAX_WAIT ]; do
# Get the latest CI workflow run for this commit
RUN_ID=$(gh run list \
--workflow="$WORKFLOW_NAME" \
--commit="$COMMIT_SHA" \
--repo="$REPO" \
--json databaseId,conclusion,status \
--jq '.[0].databaseId' 2>/dev/null || echo "")
if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then
echo "β³ CI workflow not found yet, waiting..."
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
continue
fi
# Get workflow status
STATUS=$(gh run view "$RUN_ID" \
--repo="$REPO" \
--json status,conclusion \
--jq '.status' 2>/dev/null || echo "unknown")
CONCLUSION=$(gh run view "$RUN_ID" \
--repo="$REPO" \
--json conclusion \
--jq '.conclusion' 2>/dev/null || echo "unknown")
echo "π CI Status: $STATUS, Conclusion: $CONCLUSION"
if [ "$STATUS" == "completed" ]; then
if [ "$CONCLUSION" == "success" ]; then
echo "β
CI tests passed! Proceeding with release creation."
break
else
echo "β CI tests failed or were cancelled. Aborting release."
echo "Please fix the failing tests before releasing."
exit 1
fi
fi
echo "β³ CI still running, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
if [ $ELAPSED -ge $MAX_WAIT ]; then
echo "β οΈ Timeout waiting for CI to complete. Proceeding anyway, but this is unusual."
echo "Please verify CI status manually: https://github.com/$REPO/actions"
fi
- name: Check if release already exists
id: release-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Release v${{ needs.check-version.outputs.version }} already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Release v${{ needs.check-version.outputs.version }} does not exist"
fi
- name: Create ZIP file
if: steps.release-check.outputs.exists == 'false'
run: |
rm -f wiim.zip
# HACS with zip_release: true expects integration files at ZIP root
# HACS creates custom_components/wiim/ and extracts ZIP contents there
cd custom_components/wiim
zip -r ../../wiim.zip . --exclude "*/__pycache__/*" --exclude "*.pyc"
cd ../../
# Add metadata files at ZIP root (optional)
zip wiim.zip LICENSE README.md
- name: Extract changelog for this version
if: steps.release-check.outputs.exists == 'false'
id: changelog
run: |
if [ -f "CHANGELOG.md" ]; then
awk '/^## \['"${{ needs.check-version.outputs.version }}"'\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md > release_notes.md
if [ -s release_notes.md ]; then
printf 'changelog<<EOF\n' >> $GITHUB_OUTPUT
cat release_notes.md >> $GITHUB_OUTPUT
printf 'EOF\n' >> $GITHUB_OUTPUT
else
echo "changelog=Release v${{ needs.check-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
else
echo "changelog=Release v${{ needs.check-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Create Release with GitHub CLI
if: steps.release-check.outputs.exists == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NOTES: ${{ steps.changelog.outputs.changelog }}
run: |
# Double-check that release doesn't exist before creating
if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then
echo "Release v${{ needs.check-version.outputs.version }} already exists, skipping creation"
exit 0
fi
gh release create "v${{ needs.check-version.outputs.version }}" \
--title "WiiM Audio v${{ needs.check-version.outputs.version }}" \
--notes "$RELEASE_NOTES" \
--latest \
wiim.zip || {
if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then
echo "Release already exists (created by another process), continuing..."
exit 0
else
echo "Failed to create release"
exit 1
fi
}
- name: Success notification
if: steps.release-check.outputs.exists == 'false'
run: |
echo "π Successfully created release v${{ needs.check-version.outputs.version }}"
echo "π¦ ZIP file uploaded as wiim.zip"
echo "π HACS will detect this release within 24 hours"
echo "π Release URL: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.version }}"
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
# Enable automatic release workflow name: Auto Release on: push: branches: - main paths: - "custom_components/wiim/manifest.json" workflow_dispatch: inputs: reason: description: "Manual trigger reason" required: false type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check-version: timeout-minutes: 30 runs-on: latchkey-small outputs: version: ${{ steps.version.outputs.version }} version-changed: ${{ steps.check.outputs.changed }} steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 2 - name: Get current version id: version run: | VERSION=$(grep '"version"' custom_components/wiim/manifest.json | sed 's/.*"version": "\(.*\)".*/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Current version: $VERSION" - name: Get previous version id: prev-version run: | git checkout HEAD~1 PREV_VERSION=$(grep '"version"' custom_components/wiim/manifest.json | sed 's/.*"version": "\(.*\)".*/\1/' || echo "none") echo "prev-version=$PREV_VERSION" >> $GITHUB_OUTPUT echo "Previous version: $PREV_VERSION" git checkout - - name: Check if version changed id: check run: | if [ "${{ steps.version.outputs.version }}" != "${{ steps.prev-version.outputs.prev-version }}" ]; then echo "changed=true" >> $GITHUB_OUTPUT echo "Version changed from ${{ steps.prev-version.outputs.prev-version }} to ${{ steps.version.outputs.version }}" else echo "changed=false" >> $GITHUB_OUTPUT echo "Version unchanged" fi # HACS validation - only needed for releases # Note: Tests and linting are handled by the CI workflow, which runs on every push. # This workflow only runs when manifest.json changes, so CI will have already validated the code. hacs-validation: timeout-minutes: 30 needs: check-version if: needs.check-version.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' runs-on: latchkey-small steps: - name: Checkout repository uses: actions/checkout@v6 # --- HACS repository validation --------------------------------------------------- # Validate that the repository follows all rules required by HACS. # This uses the official HACS Action (https://github.com/hacs/action) # so that every push and every automated release is guaranteed to be # accepted by HACS without manual re-validation. - name: Validate repository with HACS Action uses: hacs/action@22.5.0 with: category: "integration" ignore: "topics brands" release: timeout-minutes: 30 needs: [check-version, hacs-validation] if: needs.check-version.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' runs-on: latchkey-small permissions: contents: write actions: read steps: - name: Checkout repository uses: actions/checkout@v6 - name: Wait for CI tests to complete env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "β³ Waiting for CI tests to complete before creating release..." echo "This ensures the release is only created after all tests pass." # Get the commit SHA that triggered this workflow COMMIT_SHA="${{ github.sha }}" WORKFLOW_NAME="CI" REPO="${{ github.repository }}" # Wait up to 15 minutes for CI to complete MAX_WAIT=900 ELAPSED=0 INTERVAL=10 while [ $ELAPSED -lt $MAX_WAIT ]; do # Get the latest CI workflow run for this commit RUN_ID=$(gh run list \ --workflow="$WORKFLOW_NAME" \ --commit="$COMMIT_SHA" \ --repo="$REPO" \ --json databaseId,conclusion,status \ --jq '.[0].databaseId' 2>/dev/null || echo "") if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then echo "β³ CI workflow not found yet, waiting..." sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) continue fi # Get workflow status STATUS=$(gh run view "$RUN_ID" \ --repo="$REPO" \ --json status,conclusion \ --jq '.status' 2>/dev/null || echo "unknown") CONCLUSION=$(gh run view "$RUN_ID" \ --repo="$REPO" \ --json conclusion \ --jq '.conclusion' 2>/dev/null || echo "unknown") echo "π CI Status: $STATUS, Conclusion: $CONCLUSION" if [ "$STATUS" == "completed" ]; then if [ "$CONCLUSION" == "success" ]; then echo "β CI tests passed! Proceeding with release creation." break else echo "β CI tests failed or were cancelled. Aborting release." echo "Please fix the failing tests before releasing." exit 1 fi fi echo "β³ CI still running, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) done if [ $ELAPSED -ge $MAX_WAIT ]; then echo "β οΈ Timeout waiting for CI to complete. Proceeding anyway, but this is unusual." echo "Please verify CI status manually: https://github.com/$REPO/actions" fi - name: Check if release already exists id: release-check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "Release v${{ needs.check-version.outputs.version }} already exists" else echo "exists=false" >> $GITHUB_OUTPUT echo "Release v${{ needs.check-version.outputs.version }} does not exist" fi - name: Create ZIP file if: steps.release-check.outputs.exists == 'false' run: | rm -f wiim.zip # HACS with zip_release: true expects integration files at ZIP root # HACS creates custom_components/wiim/ and extracts ZIP contents there cd custom_components/wiim zip -r ../../wiim.zip . --exclude "*/__pycache__/*" --exclude "*.pyc" cd ../../ # Add metadata files at ZIP root (optional) zip wiim.zip LICENSE README.md - name: Extract changelog for this version if: steps.release-check.outputs.exists == 'false' id: changelog run: | if [ -f "CHANGELOG.md" ]; then awk '/^## \['"${{ needs.check-version.outputs.version }}"'\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md > release_notes.md if [ -s release_notes.md ]; then printf 'changelog<<EOF\n' >> $GITHUB_OUTPUT cat release_notes.md >> $GITHUB_OUTPUT printf 'EOF\n' >> $GITHUB_OUTPUT else echo "changelog=Release v${{ needs.check-version.outputs.version }}" >> $GITHUB_OUTPUT fi else echo "changelog=Release v${{ needs.check-version.outputs.version }}" >> $GITHUB_OUTPUT fi - name: Create Release with GitHub CLI if: steps.release-check.outputs.exists == 'false' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_NOTES: ${{ steps.changelog.outputs.changelog }} run: | # Double-check that release doesn't exist before creating if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then echo "Release v${{ needs.check-version.outputs.version }} already exists, skipping creation" exit 0 fi gh release create "v${{ needs.check-version.outputs.version }}" \ --title "WiiM Audio v${{ needs.check-version.outputs.version }}" \ --notes "$RELEASE_NOTES" \ --latest \ wiim.zip || { if gh release view "v${{ needs.check-version.outputs.version }}" >/dev/null 2>&1; then echo "Release already exists (created by another process), continuing..." exit 0 else echo "Failed to create release" exit 1 fi } - name: Success notification if: steps.release-check.outputs.exists == 'false' run: | echo "π Successfully created release v${{ needs.check-version.outputs.version }}" echo "π¦ ZIP file uploaded as wiim.zip" echo "π HACS will detect this release within 24 hours" echo "π Release URL: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.version }}"
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.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.