Skip to content
Latchkey

Release workflow (ruaan-deysel/ha-unraid)

The Release workflow from ruaan-deysel/ha-unraid, explained and optimized by Latchkey.

C

CI health: C - fair

Point runs-on at Latchkey and get run de-duplication, 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: ruaan-deysel/ha-unraid.github/workflows/release.ymlLicense Apache-2.0View source

What it does

This is the Release workflow from the ruaan-deysel/ha-unraid repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
---
# yamllint disable rule:line-length
name: Release

"on":
  push:
    branches:
      - main
    paths:
      - custom_components/unraid/manifest.json
    tags:
      - "v*"

permissions: {}

jobs:
  auto_tag:
    name: Tag release on manifest change
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/heads/')
    permissions:
      contents: write
    outputs:
      version: ${{ steps.manifest.outputs.version }}
      tag_created: ${{ steps.create_tag.outputs.created }}

    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      - name: Read version from manifest
        id: manifest
        run: |
          VERSION=$(python3 -c "import json; print(json.load(open('custom_components/unraid/manifest.json'))['version'])")

          if [ -z "$VERSION" ]; then
            echo "Version not found in manifest.json"
            exit 1
          fi

          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Manifest version: $VERSION"

      # Only release once the CHANGELOG is ready: the [Unreleased] content
      # must have been moved under a "## [VERSION]" heading matching the new
      # manifest version. A manifest bump without that section skips tagging
      # instead of publishing a release with generic fallback notes. Because
      # this workflow only triggers on manifest.json changes, the changelog
      # section must land in the same push as (or before) the manifest bump.
      - name: Check CHANGELOG has notes for this version
        id: changelog
        run: |
          VERSION="${{ steps.manifest.outputs.version }}"

          if [ ! -f "CHANGELOG.md" ]; then
            echo "ready=false" >> $GITHUB_OUTPUT
            echo "CHANGELOG.md not found"
            exit 0
          fi

          # Escape regex metacharacters (the dots) in the version string
          VER_RE=$(printf '%s' "$VERSION" | sed 's/\./\\./g')
          NOTES=$(awk -v ver="$VER_RE" '
            $0 ~ "^## \\[" ver "\\]" {in_section=1; next}
            in_section && $0 ~ /^## \[/ {exit}
            in_section {print}
          ' CHANGELOG.md)

          if [ -n "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
            echo "ready=true" >> $GITHUB_OUTPUT
            echo "Found release notes for $VERSION in CHANGELOG.md"
          else
            echo "ready=false" >> $GITHUB_OUTPUT
            echo "No '## [$VERSION]' section with content found in CHANGELOG.md"
          fi

      - name: Check if tag already exists
        id: check_tag
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"

          if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
            echo "exists=true" >> $GITHUB_OUTPUT
            echo "Tag $TAG already exists (local)"
            exit 0
          fi

          if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then
            echo "exists=true" >> $GITHUB_OUTPUT
            echo "Tag $TAG already exists (remote)"
          else
            echo "exists=false" >> $GITHUB_OUTPUT
            echo "Tag $TAG does not exist"
          fi

      - name: Create and push tag
        id: create_tag
        if: >-
          steps.check_tag.outputs.exists == 'false' &&
          steps.changelog.outputs.ready == 'true'
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"
          echo "Creating tag $TAG"

          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          git tag -a "$TAG" -m "Release $TAG"
          git push origin "$TAG"

          echo "created=true" >> $GITHUB_OUTPUT

      - name: Summary
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"
          VERSION="${{ steps.manifest.outputs.version }}"
          if [ "${{ steps.check_tag.outputs.exists }}" = "true" ]; then
            echo "Tag $TAG already existed. No new tag created." >> $GITHUB_STEP_SUMMARY
          elif [ "${{ steps.changelog.outputs.ready }}" != "true" ]; then
            echo "⚠️ Tag $TAG **not** created: CHANGELOG.md has no \`## [$VERSION]\` section with content." >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "Move the \`[Unreleased]\` notes under a \`## [$VERSION]\` heading and push the change together with a manifest.json update to trigger the release." >> $GITHUB_STEP_SUMMARY
          else
            echo "Created and pushed tag $TAG from manifest.json version." >> $GITHUB_STEP_SUMMARY
            echo "The release job in this workflow run will now build and publish the release." >> $GITHUB_STEP_SUMMARY
          fi

  # Runs in two ways:
  #   1. Same-run after auto_tag creates a tag from a manifest version bump.
  #      Tags pushed with the default GITHUB_TOKEN do NOT trigger new workflow
  #      runs (GitHub's recursion guard), so the release must be built here -
  #      a separate tag-triggered run would never start.
  #   2. Directly from a manually pushed v* tag (auto_tag is skipped; the
  #      `!cancelled()` keeps this job eligible despite the skipped dependency).
  release:
    name: Build and Release
    runs-on: ubuntu-latest
    needs: auto_tag
    if: >-
      !cancelled() && (
        startsWith(github.ref, 'refs/tags/') ||
        (needs.auto_tag.result == 'success' && needs.auto_tag.outputs.tag_created == 'true')
      )
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      - name: Determine version
        id: version
        run: |
          if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
            # Triggered by a manually pushed tag (e.g., v2025.12.0 -> 2025.12.0)
            VERSION=${GITHUB_REF#refs/tags/v}
          else
            # Same-run release after auto_tag created the tag
            VERSION="${{ needs.auto_tag.outputs.version }}"
          fi

          if [ -z "$VERSION" ]; then
            echo "ERROR: could not determine release version"
            exit 1
          fi

          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Version: $VERSION"

          # Check if this is a pre-release
          if [[ "$VERSION" =~ (alpha|beta|rc) ]]; then
            echo "prerelease=true" >> $GITHUB_OUTPUT
            echo "This is a pre-release"
          else
            echo "prerelease=false" >> $GITHUB_OUTPUT
            echo "This is a stable release"
          fi

      - name: Verify manifest.json version matches tag
        run: |
          MANIFEST_VERSION=$(python3 -c "import json; print(json.load(open('custom_components/unraid/manifest.json'))['version'])")
          TAG_VERSION="${{ steps.version.outputs.version }}"

          if [ "$MANIFEST_VERSION" != "$TAG_VERSION" ]; then
            echo "ERROR: manifest.json version ($MANIFEST_VERSION) does not match tag ($TAG_VERSION)"
            exit 1
          fi

          echo "✅ manifest.json version matches tag: $TAG_VERSION"

      - name: Build release package
        run: |
          echo "Building release package..."

          # Create build directory
          mkdir -p build

          # Create the integration package (zip file for HACS compatibility)
          PACKAGE_FILE="build/unraid-${{ steps.version.outputs.version }}.zip"

          # Package the custom_components directory
          cd custom_components
          zip -r "../$PACKAGE_FILE" unraid/ \
            -x "*.pyc" \
            -x "*__pycache__*" \
            -x "*.git*" \
            -x "*.DS_Store"
          cd ..

          # Verify the package was created
          if [ ! -f "$PACKAGE_FILE" ]; then
            echo "ERROR: Package file not found: $PACKAGE_FILE"
            ls -la build/
            exit 1
          fi

          echo "✅ Package created: $PACKAGE_FILE"
          ls -lh "$PACKAGE_FILE"

          # Show package contents
          echo ""
          echo "Package contents:"
          unzip -l "$PACKAGE_FILE" | head -20

      - name: Calculate SHA256 checksum
        id: checksum
        run: |
          PACKAGE_FILE="build/unraid-${{ steps.version.outputs.version }}.zip"

          # Calculate SHA256 checksum
          SHA256=$(sha256sum "$PACKAGE_FILE" | awk '{print $1}')

          echo "sha256=$SHA256" >> $GITHUB_OUTPUT
          echo "✅ SHA256 checksum: $SHA256"

          # Save checksum to file for release notes
          echo "$SHA256" > build/SHA256SUM

      - name: Extract release notes from CHANGELOG
        id: changelog
        run: |
          VERSION="${{ steps.version.outputs.version }}"

          # Check if CHANGELOG.md exists
          if [ ! -f "CHANGELOG.md" ]; then
            echo "⚠️  CHANGELOG.md not found, using default release notes"
            NOTES="Release v$VERSION of the Unraid Home Assistant integration."
          else
            # Extract release notes for this version from CHANGELOG.md
            # Look for content after "## [VERSION]" until the next version header.
            if grep -qF "## [$VERSION]" CHANGELOG.md; then
              # Escape regex metacharacters (the dots) in the version string
              VER_RE=$(printf '%s' "$VERSION" | sed 's/\./\\./g')
              NOTES=$(awk -v ver="$VER_RE" '
                $0 ~ "^## \\[" ver "\\]" {in_section=1; next}
                in_section && $0 ~ /^## \[/ {exit}
                in_section {print}
              ' CHANGELOG.md)

              if [ -z "$NOTES" ]; then
                echo "⚠️  No release notes found for version $VERSION in CHANGELOG.md"
                NOTES="See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details."
              else
                echo "✅ Extracted release notes for version $VERSION"
              fi
            else
              echo "⚠️  Version $VERSION not found in CHANGELOG.md"
              NOTES="See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details."
            fi
          fi

          # Save to file (GitHub Actions doesn't handle multiline outputs well)
          echo "$NOTES" > build/RELEASE_NOTES.md

          # Add link to full changelog if not already present in NOTES
          if ! grep -q "See \[CHANGELOG.md\](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details\." build/RELEASE_NOTES.md; then
            echo "" >> build/RELEASE_NOTES.md
            echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." >> build/RELEASE_NOTES.md
          fi

          # Add installation instructions
          echo "" >> build/RELEASE_NOTES.md
          echo "---" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "## 📦 Installation" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "### Via HACS (Recommended)" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "1. Open HACS in Home Assistant" >> build/RELEASE_NOTES.md
          echo "2. Go to **Integrations**" >> build/RELEASE_NOTES.md
          echo "3. Click the **⋮** menu → **Custom repositories**" >> build/RELEASE_NOTES.md
          echo "4. Add this repository: \`https://github.com/${{ github.repository }}\`" >> build/RELEASE_NOTES.md
          echo "5. Category: **Integration**" >> build/RELEASE_NOTES.md
          echo "6. Click **Add**" >> build/RELEASE_NOTES.md
          echo "7. Search for **Unraid**" >> build/RELEASE_NOTES.md
          echo "8. Click **Download**" >> build/RELEASE_NOTES.md
          echo "9. Restart Home Assistant" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "### Manual Installation" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "1. Download \`unraid-$VERSION.zip\`" >> build/RELEASE_NOTES.md
          echo "2. Extract to \`config/custom_components/\`" >> build/RELEASE_NOTES.md
          echo "3. Restart Home Assistant" >> build/RELEASE_NOTES.md
          echo "4. Add the integration via **Settings** → **Devices & Services** → **Add Integration**" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md

          # Add SHA256 checksum to release notes
          echo "---" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "**SHA256 Checksum:**" >> build/RELEASE_NOTES.md
          echo "\`\`\`" >> build/RELEASE_NOTES.md
          echo "${{ steps.checksum.outputs.sha256 }}  unraid-$VERSION.zip" >> build/RELEASE_NOTES.md
          echo "\`\`\`" >> build/RELEASE_NOTES.md

          cat build/RELEASE_NOTES.md

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="v${{ steps.version.outputs.version }}"
          PRE_FLAG=""

          if [ "${{ steps.version.outputs.prerelease }}" = "true" ]; then
            PRE_FLAG="--prerelease"
          fi

          gh release create "$TAG" \
            build/unraid-${{ steps.version.outputs.version }}.zip \
            build/SHA256SUM \
            CHANGELOG.md \
            --title "$TAG" \
            --notes-file build/RELEASE_NOTES.md \
            $PRE_FLAG

      - name: Release Summary
        run: |
          echo "## 🎉 v${{ steps.version.outputs.version }} Released Successfully!" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Package:** \`unraid-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**SHA256 Checksum:** \`${{ steps.checksum.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Release URL:** https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "${{ steps.version.outputs.prerelease }}" == "true" ]; then
            echo "**Type:** Pre-release" >> $GITHUB_STEP_SUMMARY
          else
            echo "**Type:** Stable release" >> $GITHUB_STEP_SUMMARY
          fi

          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### 📋 Next Steps" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Release created and package uploaded" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ HACS users can now update to v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Manual installation package available for download" >> $GITHUB_STEP_SUMMARY

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

---
# yamllint disable rule:line-length
name: Release
 
"on":
  push:
    branches:
      - main
    paths:
      - custom_components/unraid/manifest.json
    tags:
      - "v*"
 
permissions: {}
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  auto_tag:
    timeout-minutes: 30
    name: Tag release on manifest change
    runs-on: latchkey-small
    if: startsWith(github.ref, 'refs/heads/')
    permissions:
      contents: write
    outputs:
      version: ${{ steps.manifest.outputs.version }}
      tag_created: ${{ steps.create_tag.outputs.created }}
 
    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0
 
      - name: Read version from manifest
        id: manifest
        run: |
          VERSION=$(python3 -c "import json; print(json.load(open('custom_components/unraid/manifest.json'))['version'])")
 
          if [ -z "$VERSION" ]; then
            echo "Version not found in manifest.json"
            exit 1
          fi
 
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Manifest version: $VERSION"
 
      # Only release once the CHANGELOG is ready: the [Unreleased] content
      # must have been moved under a "## [VERSION]" heading matching the new
      # manifest version. A manifest bump without that section skips tagging
      # instead of publishing a release with generic fallback notes. Because
      # this workflow only triggers on manifest.json changes, the changelog
      # section must land in the same push as (or before) the manifest bump.
      - name: Check CHANGELOG has notes for this version
        id: changelog
        run: |
          VERSION="${{ steps.manifest.outputs.version }}"
 
          if [ ! -f "CHANGELOG.md" ]; then
            echo "ready=false" >> $GITHUB_OUTPUT
            echo "CHANGELOG.md not found"
            exit 0
          fi
 
          # Escape regex metacharacters (the dots) in the version string
          VER_RE=$(printf '%s' "$VERSION" | sed 's/\./\\./g')
          NOTES=$(awk -v ver="$VER_RE" '
            $0 ~ "^## \\[" ver "\\]" {in_section=1; next}
            in_section && $0 ~ /^## \[/ {exit}
            in_section {print}
          ' CHANGELOG.md)
 
          if [ -n "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
            echo "ready=true" >> $GITHUB_OUTPUT
            echo "Found release notes for $VERSION in CHANGELOG.md"
          else
            echo "ready=false" >> $GITHUB_OUTPUT
            echo "No '## [$VERSION]' section with content found in CHANGELOG.md"
          fi
 
      - name: Check if tag already exists
        id: check_tag
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"
 
          if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
            echo "exists=true" >> $GITHUB_OUTPUT
            echo "Tag $TAG already exists (local)"
            exit 0
          fi
 
          if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then
            echo "exists=true" >> $GITHUB_OUTPUT
            echo "Tag $TAG already exists (remote)"
          else
            echo "exists=false" >> $GITHUB_OUTPUT
            echo "Tag $TAG does not exist"
          fi
 
      - name: Create and push tag
        id: create_tag
        if: >-
          steps.check_tag.outputs.exists == 'false' &&
          steps.changelog.outputs.ready == 'true'
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"
          echo "Creating tag $TAG"
 
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
 
          git tag -a "$TAG" -m "Release $TAG"
          git push origin "$TAG"
 
          echo "created=true" >> $GITHUB_OUTPUT
 
      - name: Summary
        run: |
          TAG="v${{ steps.manifest.outputs.version }}"
          VERSION="${{ steps.manifest.outputs.version }}"
          if [ "${{ steps.check_tag.outputs.exists }}" = "true" ]; then
            echo "Tag $TAG already existed. No new tag created." >> $GITHUB_STEP_SUMMARY
          elif [ "${{ steps.changelog.outputs.ready }}" != "true" ]; then
            echo "⚠️ Tag $TAG **not** created: CHANGELOG.md has no \`## [$VERSION]\` section with content." >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "Move the \`[Unreleased]\` notes under a \`## [$VERSION]\` heading and push the change together with a manifest.json update to trigger the release." >> $GITHUB_STEP_SUMMARY
          else
            echo "Created and pushed tag $TAG from manifest.json version." >> $GITHUB_STEP_SUMMARY
            echo "The release job in this workflow run will now build and publish the release." >> $GITHUB_STEP_SUMMARY
          fi
 
  # Runs in two ways:
  #   1. Same-run after auto_tag creates a tag from a manifest version bump.
  #      Tags pushed with the default GITHUB_TOKEN do NOT trigger new workflow
  #      runs (GitHub's recursion guard), so the release must be built here -
  #      a separate tag-triggered run would never start.
  #   2. Directly from a manually pushed v* tag (auto_tag is skipped; the
  #      `!cancelled()` keeps this job eligible despite the skipped dependency).
  release:
    timeout-minutes: 30
    name: Build and Release
    runs-on: latchkey-small
    needs: auto_tag
    if: >-
      !cancelled() && (
        startsWith(github.ref, 'refs/tags/') ||
        (needs.auto_tag.result == 'success' && needs.auto_tag.outputs.tag_created == 'true')
      )
    permissions:
      contents: write
 
    steps:
      - name: Checkout code
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0
 
      - name: Determine version
        id: version
        run: |
          if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
            # Triggered by a manually pushed tag (e.g., v2025.12.0 -> 2025.12.0)
            VERSION=${GITHUB_REF#refs/tags/v}
          else
            # Same-run release after auto_tag created the tag
            VERSION="${{ needs.auto_tag.outputs.version }}"
          fi
 
          if [ -z "$VERSION" ]; then
            echo "ERROR: could not determine release version"
            exit 1
          fi
 
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Version: $VERSION"
 
          # Check if this is a pre-release
          if [[ "$VERSION" =~ (alpha|beta|rc) ]]; then
            echo "prerelease=true" >> $GITHUB_OUTPUT
            echo "This is a pre-release"
          else
            echo "prerelease=false" >> $GITHUB_OUTPUT
            echo "This is a stable release"
          fi
 
      - name: Verify manifest.json version matches tag
        run: |
          MANIFEST_VERSION=$(python3 -c "import json; print(json.load(open('custom_components/unraid/manifest.json'))['version'])")
          TAG_VERSION="${{ steps.version.outputs.version }}"
 
          if [ "$MANIFEST_VERSION" != "$TAG_VERSION" ]; then
            echo "ERROR: manifest.json version ($MANIFEST_VERSION) does not match tag ($TAG_VERSION)"
            exit 1
          fi
 
          echo "✅ manifest.json version matches tag: $TAG_VERSION"
 
      - name: Build release package
        run: |
          echo "Building release package..."
 
          # Create build directory
          mkdir -p build
 
          # Create the integration package (zip file for HACS compatibility)
          PACKAGE_FILE="build/unraid-${{ steps.version.outputs.version }}.zip"
 
          # Package the custom_components directory
          cd custom_components
          zip -r "../$PACKAGE_FILE" unraid/ \
            -x "*.pyc" \
            -x "*__pycache__*" \
            -x "*.git*" \
            -x "*.DS_Store"
          cd ..
 
          # Verify the package was created
          if [ ! -f "$PACKAGE_FILE" ]; then
            echo "ERROR: Package file not found: $PACKAGE_FILE"
            ls -la build/
            exit 1
          fi
 
          echo "✅ Package created: $PACKAGE_FILE"
          ls -lh "$PACKAGE_FILE"
 
          # Show package contents
          echo ""
          echo "Package contents:"
          unzip -l "$PACKAGE_FILE" | head -20
 
      - name: Calculate SHA256 checksum
        id: checksum
        run: |
          PACKAGE_FILE="build/unraid-${{ steps.version.outputs.version }}.zip"
 
          # Calculate SHA256 checksum
          SHA256=$(sha256sum "$PACKAGE_FILE" | awk '{print $1}')
 
          echo "sha256=$SHA256" >> $GITHUB_OUTPUT
          echo "✅ SHA256 checksum: $SHA256"
 
          # Save checksum to file for release notes
          echo "$SHA256" > build/SHA256SUM
 
      - name: Extract release notes from CHANGELOG
        id: changelog
        run: |
          VERSION="${{ steps.version.outputs.version }}"
 
          # Check if CHANGELOG.md exists
          if [ ! -f "CHANGELOG.md" ]; then
            echo "⚠️  CHANGELOG.md not found, using default release notes"
            NOTES="Release v$VERSION of the Unraid Home Assistant integration."
          else
            # Extract release notes for this version from CHANGELOG.md
            # Look for content after "## [VERSION]" until the next version header.
            if grep -qF "## [$VERSION]" CHANGELOG.md; then
              # Escape regex metacharacters (the dots) in the version string
              VER_RE=$(printf '%s' "$VERSION" | sed 's/\./\\./g')
              NOTES=$(awk -v ver="$VER_RE" '
                $0 ~ "^## \\[" ver "\\]" {in_section=1; next}
                in_section && $0 ~ /^## \[/ {exit}
                in_section {print}
              ' CHANGELOG.md)
 
              if [ -z "$NOTES" ]; then
                echo "⚠️  No release notes found for version $VERSION in CHANGELOG.md"
                NOTES="See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details."
              else
                echo "✅ Extracted release notes for version $VERSION"
              fi
            else
              echo "⚠️  Version $VERSION not found in CHANGELOG.md"
              NOTES="See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details."
            fi
          fi
 
          # Save to file (GitHub Actions doesn't handle multiline outputs well)
          echo "$NOTES" > build/RELEASE_NOTES.md
 
          # Add link to full changelog if not already present in NOTES
          if ! grep -q "See \[CHANGELOG.md\](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details\." build/RELEASE_NOTES.md; then
            echo "" >> build/RELEASE_NOTES.md
            echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." >> build/RELEASE_NOTES.md
          fi
 
          # Add installation instructions
          echo "" >> build/RELEASE_NOTES.md
          echo "---" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "## 📦 Installation" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "### Via HACS (Recommended)" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "1. Open HACS in Home Assistant" >> build/RELEASE_NOTES.md
          echo "2. Go to **Integrations**" >> build/RELEASE_NOTES.md
          echo "3. Click the **⋮** menu → **Custom repositories**" >> build/RELEASE_NOTES.md
          echo "4. Add this repository: \`https://github.com/${{ github.repository }}\`" >> build/RELEASE_NOTES.md
          echo "5. Category: **Integration**" >> build/RELEASE_NOTES.md
          echo "6. Click **Add**" >> build/RELEASE_NOTES.md
          echo "7. Search for **Unraid**" >> build/RELEASE_NOTES.md
          echo "8. Click **Download**" >> build/RELEASE_NOTES.md
          echo "9. Restart Home Assistant" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "### Manual Installation" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "1. Download \`unraid-$VERSION.zip\`" >> build/RELEASE_NOTES.md
          echo "2. Extract to \`config/custom_components/\`" >> build/RELEASE_NOTES.md
          echo "3. Restart Home Assistant" >> build/RELEASE_NOTES.md
          echo "4. Add the integration via **Settings** → **Devices & Services** → **Add Integration**" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
 
          # Add SHA256 checksum to release notes
          echo "---" >> build/RELEASE_NOTES.md
          echo "" >> build/RELEASE_NOTES.md
          echo "**SHA256 Checksum:**" >> build/RELEASE_NOTES.md
          echo "\`\`\`" >> build/RELEASE_NOTES.md
          echo "${{ steps.checksum.outputs.sha256 }}  unraid-$VERSION.zip" >> build/RELEASE_NOTES.md
          echo "\`\`\`" >> build/RELEASE_NOTES.md
 
          cat build/RELEASE_NOTES.md
 
      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="v${{ steps.version.outputs.version }}"
          PRE_FLAG=""
 
          if [ "${{ steps.version.outputs.prerelease }}" = "true" ]; then
            PRE_FLAG="--prerelease"
          fi
 
          gh release create "$TAG" \
            build/unraid-${{ steps.version.outputs.version }}.zip \
            build/SHA256SUM \
            CHANGELOG.md \
            --title "$TAG" \
            --notes-file build/RELEASE_NOTES.md \
            $PRE_FLAG
 
      - name: Release Summary
        run: |
          echo "## 🎉 v${{ steps.version.outputs.version }} Released Successfully!" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Package:** \`unraid-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**SHA256 Checksum:** \`${{ steps.checksum.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Release URL:** https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
 
          if [ "${{ steps.version.outputs.prerelease }}" == "true" ]; then
            echo "**Type:** Pre-release" >> $GITHUB_STEP_SUMMARY
          else
            echo "**Type:** Stable release" >> $GITHUB_STEP_SUMMARY
          fi
 
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### 📋 Next Steps" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Release created and package uploaded" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ HACS users can now update to v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "- ✅ Manual installation package available for download" >> $GITHUB_STEP_SUMMARY
 

What changed

This workflow runs 2 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