Build and Release workflow (HolmesGPT/holmesgpt)
The Build and Release workflow from HolmesGPT/holmesgpt, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build and Release workflow from the HolmesGPT/holmesgpt 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
name: Build and Release
on:
release:
types: [created]
jobs:
build:
strategy:
matrix:
include:
# Linux AMD64 - build on older Ubuntu for forward compatibility
# See https://pyinstaller.org/en/stable/usage.html?highlight=glibc#making-gnu-linux-apps-forward-compatible
- os: ubuntu-22.04
platform: linux
arch: amd64
# macOS ARM64 (Apple Silicon) - macos-14+ runs on M1/M2
- os: macos-14
platform: darwin
arch: arm64
# Windows AMD64
- os: windows-latest
platform: windows
arch: amd64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install dependencies (Unix)
if: matrix.platform != 'windows'
run: |
python -m pip install --upgrade pip "setuptools<75" "pyinstaller>=6.11"
curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0
poetry config virtualenvs.create false
poetry install --no-root
- name: Install dependencies (Windows)
if: matrix.platform == 'windows'
run: |
python -m pip install --upgrade pip "setuptools<75" "pyinstaller>=6.11"
curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0
C:\Users\runneradmin\AppData\Roaming\Python\Scripts\poetry config virtualenvs.create false
C:\Users\runneradmin\AppData\Roaming\Python\Scripts\poetry install --no-root
- name: Install dependencies (Linux)
if: matrix.platform == 'linux'
run: |
sudo apt-get install -y binutils
- name: Update package version (Linux)
if: matrix.platform == 'linux'
run: sed -i 's/__version__ = .*/__version__ = "${{ github.ref_name }}"/g' holmes/__init__.py
# mac has BSD style sed command where you specify -i '' and not just -i
- name: Update package version (macOS)
if: matrix.platform == 'darwin'
run: sed -i '' 's/__version__ = .*/__version__ = "${{ github.ref_name }}"/g' holmes/__init__.py
# windows has no sed, so we use powershell
- name: Update package version (Windows)
if: matrix.platform == 'windows'
run: |
$filePath = 'holmes/__init__.py'
(Get-Content $filePath) -replace '__version__ = .+', '__version__ = "${{ github.ref_name }}"' | Set-Content $filePath
shell: pwsh
- name: Install unixODBC (macOS)
if: matrix.platform == 'darwin'
run: brew install unixodbc
# if you change something here, you must also change it in .github/workflows/build-and-test.yaml
- name: Build with PyInstaller
shell: bash
# regarding the tiktoken part of the command, see https://github.com/openai/tiktoken/issues/80
# regarding the litellm part of the command, see https://github.com/pyinstaller/pyinstaller/issues/8620#issuecomment-2186540504
run: |
pyinstaller holmes_cli.py \
--name holmes \
--add-data 'holmes/plugins/skills/*:holmes/plugins/skills' \
--add-data 'holmes/plugins/prompts/*:holmes/plugins/prompts' \
--add-data 'holmes/plugins/toolsets/*:holmes/plugins/toolsets' \
--add-data 'holmes/plugins/toolsets/coralogix*:holmes/plugins/toolsets/coralogix' \
--add-data 'holmes/plugins/toolsets/grafana*:holmes/plugins/toolsets/grafana' \
--add-data 'holmes/plugins/toolsets/internet*:holmes/plugins/toolsets/internet' \
--add-data 'holmes/plugins/toolsets/elasticsearch*:holmes/plugins/toolsets/elasticsearch' \
--add-data 'holmes/plugins/toolsets/prometheus*:holmes/plugins/toolsets/prometheus' \
--hidden-import=tiktoken_ext.openai_public \
--hidden-import=tiktoken_ext \
--hidden-import=backports \
--hiddenimport litellm.llms.tokenizers \
--hiddenimport litellm.litellm_core_utils.tokenizers \
--collect-data litellm
ls dist
- name: Zip the application (Unix and Mac)
if: matrix.platform != 'windows'
run: |
cd dist
zip -r --symlinks holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip holmes
mv holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip ../
cd ..
- name: Zip the application (Windows)
if: matrix.platform == 'windows'
run: |
Set-Location -Path dist
Compress-Archive -Path holmes -DestinationPath holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip -Force
Move-Item -Path holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip -Destination ..\
Set-Location -Path ..
- name: Upload Release Asset
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip
asset_name: holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip
asset_content_type: application/octet-stream
- name: Upload build as artifact
uses: actions/upload-artifact@v4
with:
name: holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}
path: ./holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip
check-latest:
needs: build
runs-on: ubuntu-22.04
outputs:
IS_LATEST: ${{ steps.check-latest.outputs.release == github.ref_name }}
steps:
- id: check-latest
uses: pozetroninc/github-action-get-latest-release@v0.7.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
excludes: prerelease, draft
# Define macOS ARM64 hash job (Apple Silicon)
mac-arm64-hash:
needs: check-latest
runs-on: ubuntu-latest
if: needs.check-latest.outputs.IS_LATEST
outputs:
MAC_ARM64_BUILD_HASH: ${{ steps.calc-hash.outputs.MAC_ARM64_BUILD_HASH }}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Download macOS ARM64 artifact
uses: actions/download-artifact@v4
with:
name: holmes-darwin-arm64-${{ github.ref_name }}
- name: Calculate hash
id: calc-hash
run: echo "MAC_ARM64_BUILD_HASH=$(sha256sum holmes-darwin-arm64-${{ github.ref_name }}.zip | awk '{print $1}')" >> $GITHUB_OUTPUT
# Define Linux hash job
linux-hash:
needs: check-latest
runs-on: ubuntu-latest
if: needs.check-latest.outputs.IS_LATEST
outputs:
LINUX_BUILD_HASH: ${{ steps.calc-hash.outputs.LINUX_BUILD_HASH }}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Download Linux artifact
uses: actions/download-artifact@v4
with:
name: holmes-linux-amd64-${{ github.ref_name }}
- name: Calculate hash
id: calc-hash
run: echo "LINUX_BUILD_HASH=$(sha256sum holmes-linux-amd64-${{ github.ref_name }}.zip | awk '{print $1}')" >> $GITHUB_OUTPUT
# Update Homebrew formula with new binary URLs and hashes
update-formula:
needs: [mac-arm64-hash, linux-hash]
runs-on: ubuntu-latest
if: ${{ !github.event.release.prerelease }}
steps:
- name: Checkout homebrew-holmesgpt repository
uses: actions/checkout@v2
with:
repository: robusta-dev/homebrew-holmesgpt
token: ${{ secrets.MULTIREPO_GITHUB_TOKEN }}
- name: Update holmesgpt.rb formula
env:
MAC_ARM64_BUILD_HASH: ${{ needs.mac-arm64-hash.outputs.MAC_ARM64_BUILD_HASH }}
LINUX_BUILD_HASH: ${{ needs.linux-hash.outputs.LINUX_BUILD_HASH }}
TAG_NAME: ${{ github.ref_name }}
run: |
# Update URLs and hashes using Python for reliability
python3 << 'PYTHON_SCRIPT'
import os
import re
mac_arm64_hash = os.environ['MAC_ARM64_BUILD_HASH']
linux_hash = os.environ['LINUX_BUILD_HASH']
tag = os.environ['TAG_NAME']
with open('./Formula/holmesgpt.rb', 'r') as f:
content = f.read()
# Update macOS URL (handles both old and new naming)
content = re.sub(
r'(if OS\.mac\?\s+)url "[^"]+"',
rf'\1url "https://github.com/HolmesGPT/holmesgpt/releases/download/{tag}/holmes-darwin-arm64-{tag}.zip"',
content,
flags=re.DOTALL
)
# Update macOS sha256
content = re.sub(
r'(if OS\.mac\?\s+url "[^"]+"\s+)sha256 "[^"]+"',
rf'\1sha256 "{mac_arm64_hash}"',
content,
flags=re.DOTALL
)
# Update Linux URL (handles both old and new naming)
content = re.sub(
r'(elsif OS\.linux\?\s+)url "[^"]+"',
rf'\1url "https://github.com/HolmesGPT/holmesgpt/releases/download/{tag}/holmes-linux-amd64-{tag}.zip"',
content,
flags=re.DOTALL
)
# Update Linux sha256
content = re.sub(
r'(elsif OS\.linux\?\s+url "[^"]+"\s+)sha256 "[^"]+"',
rf'\1sha256 "{linux_hash}"',
content,
flags=re.DOTALL
)
with open('./Formula/holmesgpt.rb', 'w') as f:
f.write(content)
print("Updated formula:")
print(content)
PYTHON_SCRIPT
- name: Commit and push changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -am "Update formula for release ${{ github.ref_name }}"
git push
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: Build and Release on: release: types: [created] jobs: build: timeout-minutes: 30 strategy: matrix: include: # Linux AMD64 - build on older Ubuntu for forward compatibility # See https://pyinstaller.org/en/stable/usage.html?highlight=glibc#making-gnu-linux-apps-forward-compatible - os: ubuntu-22.04 platform: linux arch: amd64 # macOS ARM64 (Apple Silicon) - macos-14+ runs on M1/M2 - os: macos-14 platform: darwin arch: arm64 # Windows AMD64 - os: windows-latest platform: windows arch: amd64 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: cache: 'pip' python-version: '3.11' - name: Install dependencies (Unix) if: matrix.platform != 'windows' run: | python -m pip install --upgrade pip "setuptools<75" "pyinstaller>=6.11" curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0 poetry config virtualenvs.create false poetry install --no-root - name: Install dependencies (Windows) if: matrix.platform == 'windows' run: | python -m pip install --upgrade pip "setuptools<75" "pyinstaller>=6.11" curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0 C:\Users\runneradmin\AppData\Roaming\Python\Scripts\poetry config virtualenvs.create false C:\Users\runneradmin\AppData\Roaming\Python\Scripts\poetry install --no-root - name: Install dependencies (Linux) if: matrix.platform == 'linux' run: | sudo apt-get install -y binutils - name: Update package version (Linux) if: matrix.platform == 'linux' run: sed -i 's/__version__ = .*/__version__ = "${{ github.ref_name }}"/g' holmes/__init__.py # mac has BSD style sed command where you specify -i '' and not just -i - name: Update package version (macOS) if: matrix.platform == 'darwin' run: sed -i '' 's/__version__ = .*/__version__ = "${{ github.ref_name }}"/g' holmes/__init__.py # windows has no sed, so we use powershell - name: Update package version (Windows) if: matrix.platform == 'windows' run: | $filePath = 'holmes/__init__.py' (Get-Content $filePath) -replace '__version__ = .+', '__version__ = "${{ github.ref_name }}"' | Set-Content $filePath shell: pwsh - name: Install unixODBC (macOS) if: matrix.platform == 'darwin' run: brew install unixodbc # if you change something here, you must also change it in .github/workflows/build-and-test.yaml - name: Build with PyInstaller shell: bash # regarding the tiktoken part of the command, see https://github.com/openai/tiktoken/issues/80 # regarding the litellm part of the command, see https://github.com/pyinstaller/pyinstaller/issues/8620#issuecomment-2186540504 run: | pyinstaller holmes_cli.py \ --name holmes \ --add-data 'holmes/plugins/skills/*:holmes/plugins/skills' \ --add-data 'holmes/plugins/prompts/*:holmes/plugins/prompts' \ --add-data 'holmes/plugins/toolsets/*:holmes/plugins/toolsets' \ --add-data 'holmes/plugins/toolsets/coralogix*:holmes/plugins/toolsets/coralogix' \ --add-data 'holmes/plugins/toolsets/grafana*:holmes/plugins/toolsets/grafana' \ --add-data 'holmes/plugins/toolsets/internet*:holmes/plugins/toolsets/internet' \ --add-data 'holmes/plugins/toolsets/elasticsearch*:holmes/plugins/toolsets/elasticsearch' \ --add-data 'holmes/plugins/toolsets/prometheus*:holmes/plugins/toolsets/prometheus' \ --hidden-import=tiktoken_ext.openai_public \ --hidden-import=tiktoken_ext \ --hidden-import=backports \ --hiddenimport litellm.llms.tokenizers \ --hiddenimport litellm.litellm_core_utils.tokenizers \ --collect-data litellm ls dist - name: Zip the application (Unix and Mac) if: matrix.platform != 'windows' run: | cd dist zip -r --symlinks holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip holmes mv holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip ../ cd .. - name: Zip the application (Windows) if: matrix.platform == 'windows' run: | Set-Location -Path dist Compress-Archive -Path holmes -DestinationPath holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip -Force Move-Item -Path holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip -Destination ..\ Set-Location -Path .. - name: Upload Release Asset uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: ./holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip asset_name: holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip asset_content_type: application/octet-stream - name: Upload build as artifact uses: actions/upload-artifact@v4 with: name: holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }} path: ./holmes-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.ref_name }}.zip check-latest: timeout-minutes: 30 needs: build runs-on: latchkey-small outputs: IS_LATEST: ${{ steps.check-latest.outputs.release == github.ref_name }} steps: - id: check-latest uses: pozetroninc/github-action-get-latest-release@v0.7.0 with: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} excludes: prerelease, draft # Define macOS ARM64 hash job (Apple Silicon) mac-arm64-hash: timeout-minutes: 30 needs: check-latest runs-on: latchkey-small if: needs.check-latest.outputs.IS_LATEST outputs: MAC_ARM64_BUILD_HASH: ${{ steps.calc-hash.outputs.MAC_ARM64_BUILD_HASH }} steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Download macOS ARM64 artifact uses: actions/download-artifact@v4 with: name: holmes-darwin-arm64-${{ github.ref_name }} - name: Calculate hash id: calc-hash run: echo "MAC_ARM64_BUILD_HASH=$(sha256sum holmes-darwin-arm64-${{ github.ref_name }}.zip | awk '{print $1}')" >> $GITHUB_OUTPUT # Define Linux hash job linux-hash: timeout-minutes: 30 needs: check-latest runs-on: latchkey-small if: needs.check-latest.outputs.IS_LATEST outputs: LINUX_BUILD_HASH: ${{ steps.calc-hash.outputs.LINUX_BUILD_HASH }} steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Download Linux artifact uses: actions/download-artifact@v4 with: name: holmes-linux-amd64-${{ github.ref_name }} - name: Calculate hash id: calc-hash run: echo "LINUX_BUILD_HASH=$(sha256sum holmes-linux-amd64-${{ github.ref_name }}.zip | awk '{print $1}')" >> $GITHUB_OUTPUT # Update Homebrew formula with new binary URLs and hashes update-formula: timeout-minutes: 30 needs: [mac-arm64-hash, linux-hash] runs-on: latchkey-small if: ${{ !github.event.release.prerelease }} steps: - name: Checkout homebrew-holmesgpt repository uses: actions/checkout@v2 with: repository: robusta-dev/homebrew-holmesgpt token: ${{ secrets.MULTIREPO_GITHUB_TOKEN }} - name: Update holmesgpt.rb formula env: MAC_ARM64_BUILD_HASH: ${{ needs.mac-arm64-hash.outputs.MAC_ARM64_BUILD_HASH }} LINUX_BUILD_HASH: ${{ needs.linux-hash.outputs.LINUX_BUILD_HASH }} TAG_NAME: ${{ github.ref_name }} run: | # Update URLs and hashes using Python for reliability python3 << 'PYTHON_SCRIPT' import os import re mac_arm64_hash = os.environ['MAC_ARM64_BUILD_HASH'] linux_hash = os.environ['LINUX_BUILD_HASH'] tag = os.environ['TAG_NAME'] with open('./Formula/holmesgpt.rb', 'r') as f: content = f.read() # Update macOS URL (handles both old and new naming) content = re.sub( r'(if OS\.mac\?\s+)url "[^"]+"', rf'\1url "https://github.com/HolmesGPT/holmesgpt/releases/download/{tag}/holmes-darwin-arm64-{tag}.zip"', content, flags=re.DOTALL ) # Update macOS sha256 content = re.sub( r'(if OS\.mac\?\s+url "[^"]+"\s+)sha256 "[^"]+"', rf'\1sha256 "{mac_arm64_hash}"', content, flags=re.DOTALL ) # Update Linux URL (handles both old and new naming) content = re.sub( r'(elsif OS\.linux\?\s+)url "[^"]+"', rf'\1url "https://github.com/HolmesGPT/holmesgpt/releases/download/{tag}/holmes-linux-amd64-{tag}.zip"', content, flags=re.DOTALL ) # Update Linux sha256 content = re.sub( r'(elsif OS\.linux\?\s+url "[^"]+"\s+)sha256 "[^"]+"', rf'\1sha256 "{linux_hash}"', content, flags=re.DOTALL ) with open('./Formula/holmesgpt.rb', 'w') as f: f.write(content) print("Updated formula:") print(content) PYTHON_SCRIPT - name: Commit and push changes run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git commit -am "Update formula for release ${{ github.ref_name }}" git push
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. - 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.
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.
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:
- Dependency installs
- Network fetches
This workflow runs 5 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.