Build workflow (meltano/meltano)
The Build workflow from meltano/meltano, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build workflow from the meltano/meltano 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
name: Build
on:
push:
branches-ignore:
- 'gh-readonly-queue/**'
paths:
- .github/actions/docker-build-scan-push/action.yml
- .github/workflows/build.yml
- docker/meltano/Dockerfile
- src/meltano/migrations/**
- scripts/**
- pyproject.toml
workflow_dispatch:
schedule:
# Weekly on Sunday at 04:45 UTC
- cron: '45 4 * * 0'
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
build:
name: Wheel and SDist
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
name: ${{ steps.baipp.outputs.package_name }}
version: ${{ steps.baipp.outputs.package_version }}
steps:
- name: Get latest release tag (re-roll)
id: get-release-tag
if: github.event_name == 'schedule'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | # zizmor: ignore[template-injection]
tag=$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --limit 1000 --json tagName --jq '.[].tagName' | sort -V | tail -1)
if [[ -z "$tag" ]]; then
echo "::error::No stable releases found; cannot determine tag for re-roll"
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ steps.get-release-tag.outputs.tag || github.sha }}
- name: Setup uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version-file: .python-version
- name: Check version
if: github.event_name == 'release' && github.event.action == 'published'
run: |
version=$(uvx --from toml-cli toml get --toml-path=pyproject.toml project.version)
tag=$(echo "${GITHUB_REF}" | awk '{split($0,p,"/"); print p[3]}')
if [ "v$version" != "$tag" ]; then echo "Release tag ('$tag') and package version ('v$version') do not match!"; exit 1; fi;
- name: uv install
# Required to run `alembic_freeze.py`
run: |
uv sync
- name: Freeze DB
run: |
uv run scripts/alembic_freeze.py
- name: Release Marker
if: (github.event_name == 'release' && github.event.action == 'published') || github.event_name == 'schedule'
# The release marker differentiates installations 'in the wild' versus internal dev builds and tests
run: touch src/meltano/core/tracking/.release_marker
- name: Build distribution
uses: hynek/build-and-inspect-python-package@d44ca7d91762de7a7d5436ddae667c6da6d1c3df # v2.18.0
id: baipp
build_docker:
name: Docker Images (${{ matrix.slim && 'Slim' || 'Full' }}, Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: build
permissions:
actions: read # read the actions of the repo
contents: read # read the contents of the repo
packages: write # write the packages to the repo
security-events: write # create security reports for the repo
env:
DEFAULT_PYTHON: "3.10" # will be used in 'latest' images
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
slim:
- false
- true
steps:
- name: Set registry
id: set-registry
run: |
if [[ "${GITHUB_EVENT_NAME}" == "release" && "${GITHUB_EVENT_ACTION}" == "published" ]] || [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then
echo "registry=docker.io" >> "$GITHUB_OUTPUT"
else
echo "registry=ghcr.io" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_EVENT_ACTION: ${{ github.event.action }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
- name: Set the workflow inputs
id: set-inputs
env:
REGISTRY: ${{ steps.set-registry.outputs.registry }}
# This step makes it so that the same workflow inputs can be accessed
# regardless of what event triggered it.
run: |
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
- name: Get latest published release version
id: get-latest-version
if: steps.set-registry.outputs.registry == 'docker.io'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | # zizmor: ignore[template-injection]
latest=$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --limit 1000 --json tagName --jq '.[].tagName | ltrimstr("v")' | sort -V | tail -1)
echo "version=$latest" >> "$GITHUB_OUTPUT"
- name: Generate tags
id: generate-tags
run: |
extra_args=()
if [[ -n "$LATEST_VERSION" ]]; then
extra_args+=(--latest-version "$LATEST_VERSION")
fi
./scripts/generate_docker_tags.py \
--git-sha "$GITHUB_SHA" \
--package-version "$NEEDS_BUILD_OUTPUTS_VERSION" \
--python-version "$PYTHON_VERSION" \
--default-python "$DEFAULT_PYTHON" \
--registry "$STEPS_SET_INPUTS_OUTPUTS_REGISTRY" \
"${extra_args[@]}" \
> tags
env:
DOCKER_TAGS_SLIM: ${{ matrix.slim && '1' || '0' }}
GITHUB_SHA: ${{ github.sha }}
NEEDS_BUILD_OUTPUTS_VERSION: ${{ needs.build.outputs.version }}
PYTHON_VERSION: ${{ matrix.python-version }}
STEPS_SET_INPUTS_OUTPUTS_REGISTRY: ${{ steps.set-inputs.outputs.registry }}
LATEST_VERSION: ${{ steps.get-latest-version.outputs.version }}
- name: Assemble image tags
id: assemble-tags
run: |
echo "If this is not a dry run, the image will be published with the following tags:"
cat tags
echo 'IMAGE_TAGS<<EOF' >> "$GITHUB_ENV"
echo "$(cat tags)" >> "$GITHUB_ENV"
echo 'EOF' >> "$GITHUB_ENV"
- name: Set registry username and password
id: user-and-pass
run: |
if [[ "${STEPS_SET_INPUTS_OUTPUTS_REGISTRY}" == "ghcr.io" ]]; then
echo "username=${GITHUB_ACTOR}" >> "$GITHUB_OUTPUT"
echo "password=${GITHUB_TOKEN}" >> "$GITHUB_OUTPUT"
fi
if [[ "${STEPS_SET_INPUTS_OUTPUTS_REGISTRY}" == "docker.io" ]]; then
echo "username=meltano" >> "$GITHUB_OUTPUT"
echo "password=${DOCKERHUB_TOKEN}" >> "$GITHUB_OUTPUT"
fi
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
STEPS_SET_INPUTS_OUTPUTS_REGISTRY: ${{ steps.set-inputs.outputs.registry }}
- name: Build, scan, then conditionally push the Docker image for a given Python version
uses: ./.github/actions/docker-build-scan-push
with:
# Push to the container registry if
# - The trigger is a push to the main branch
# - The trigger is a workflow dispatch
# - The trigger is a release and the action is 'published'
# - The trigger is a weekly schedule (re-roll latest release with updated base image)
push: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/main' ) || ( github.event_name == 'workflow_dispatch' ) || ( github.event_name == 'release' && github.event.action == 'published' ) || github.event_name == 'schedule' }}
token: ${{ secrets.GITHUB_TOKEN }}
tags: ${{ env.IMAGE_TAGS }}
registry: ${{ steps.set-inputs.outputs.registry }}
username: ${{ steps.user-and-pass.outputs.username }}
password: ${{ steps.user-and-pass.outputs.password }}
python-version: ${{ matrix.python-version }}
slim: ${{ matrix.slim }}
pypi_release:
name: Publish to PyPI
if: github.event_name == 'release' && github.event.action == 'published'
needs: [build, build_docker]
runs-on: ubuntu-latest
permissions:
id-token: write # Needed for OIDC PyPI publishing
environment:
name: publishing
url: https://pypi.org/project/${{ needs.build.outputs.name }}/${{ needs.build.outputs.version }}
steps:
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: Packages
path: dist
- name: Publish
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
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 on: push: branches-ignore: - 'gh-readonly-queue/**' paths: - .github/actions/docker-build-scan-push/action.yml - .github/workflows/build.yml - docker/meltano/Dockerfile - src/meltano/migrations/** - scripts/** - pyproject.toml workflow_dispatch: schedule: # Weekly on Sunday at 04:45 UTC - cron: '45 4 * * 0' release: types: [published] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true permissions: {} jobs: build: timeout-minutes: 30 name: Wheel and SDist runs-on: latchkey-small permissions: contents: read outputs: name: ${{ steps.baipp.outputs.package_name }} version: ${{ steps.baipp.outputs.package_version }} steps: - name: Get latest release tag (re-roll) id: get-release-tag if: github.event_name == 'schedule' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # zizmor: ignore[template-injection] tag=$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --limit 1000 --json tagName --jq '.[].tagName' | sort -V | tail -1) if [[ -z "$tag" ]]; then echo "::error::No stable releases found; cannot determine tag for re-roll" exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Check out the repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: ${{ steps.get-release-tag.outputs.tag || github.sha }} - name: Setup uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: cache: 'pip' python-version-file: .python-version - name: Check version if: github.event_name == 'release' && github.event.action == 'published' run: | version=$(uvx --from toml-cli toml get --toml-path=pyproject.toml project.version) tag=$(echo "${GITHUB_REF}" | awk '{split($0,p,"/"); print p[3]}') if [ "v$version" != "$tag" ]; then echo "Release tag ('$tag') and package version ('v$version') do not match!"; exit 1; fi; - name: uv install # Required to run `alembic_freeze.py` run: | uv sync - name: Freeze DB run: | uv run scripts/alembic_freeze.py - name: Release Marker if: (github.event_name == 'release' && github.event.action == 'published') || github.event_name == 'schedule' # The release marker differentiates installations 'in the wild' versus internal dev builds and tests run: touch src/meltano/core/tracking/.release_marker - name: Build distribution uses: hynek/build-and-inspect-python-package@d44ca7d91762de7a7d5436ddae667c6da6d1c3df # v2.18.0 id: baipp build_docker: timeout-minutes: 30 name: Docker Images (${{ matrix.slim && 'Slim' || 'Full' }}, Python ${{ matrix.python-version }}) runs-on: latchkey-small needs: build permissions: actions: read # read the actions of the repo contents: read # read the contents of the repo packages: write # write the packages to the repo security-events: write # create security reports for the repo env: DEFAULT_PYTHON: "3.10" # will be used in 'latest' images strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" slim: - false - true steps: - name: Set registry id: set-registry run: | if [[ "${GITHUB_EVENT_NAME}" == "release" && "${GITHUB_EVENT_ACTION}" == "published" ]] || [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then echo "registry=docker.io" >> "$GITHUB_OUTPUT" else echo "registry=ghcr.io" >> "$GITHUB_OUTPUT" fi env: GITHUB_EVENT_ACTION: ${{ github.event.action }} GITHUB_EVENT_NAME: ${{ github.event_name }} - name: Set the workflow inputs id: set-inputs env: REGISTRY: ${{ steps.set-registry.outputs.registry }} # This step makes it so that the same workflow inputs can be accessed # regardless of what event triggered it. run: | echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false - name: Get latest published release version id: get-latest-version if: steps.set-registry.outputs.registry == 'docker.io' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # zizmor: ignore[template-injection] latest=$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --limit 1000 --json tagName --jq '.[].tagName | ltrimstr("v")' | sort -V | tail -1) echo "version=$latest" >> "$GITHUB_OUTPUT" - name: Generate tags id: generate-tags run: | extra_args=() if [[ -n "$LATEST_VERSION" ]]; then extra_args+=(--latest-version "$LATEST_VERSION") fi ./scripts/generate_docker_tags.py \ --git-sha "$GITHUB_SHA" \ --package-version "$NEEDS_BUILD_OUTPUTS_VERSION" \ --python-version "$PYTHON_VERSION" \ --default-python "$DEFAULT_PYTHON" \ --registry "$STEPS_SET_INPUTS_OUTPUTS_REGISTRY" \ "${extra_args[@]}" \ > tags env: DOCKER_TAGS_SLIM: ${{ matrix.slim && '1' || '0' }} GITHUB_SHA: ${{ github.sha }} NEEDS_BUILD_OUTPUTS_VERSION: ${{ needs.build.outputs.version }} PYTHON_VERSION: ${{ matrix.python-version }} STEPS_SET_INPUTS_OUTPUTS_REGISTRY: ${{ steps.set-inputs.outputs.registry }} LATEST_VERSION: ${{ steps.get-latest-version.outputs.version }} - name: Assemble image tags id: assemble-tags run: | echo "If this is not a dry run, the image will be published with the following tags:" cat tags echo 'IMAGE_TAGS<<EOF' >> "$GITHUB_ENV" echo "$(cat tags)" >> "$GITHUB_ENV" echo 'EOF' >> "$GITHUB_ENV" - name: Set registry username and password id: user-and-pass run: | if [[ "${STEPS_SET_INPUTS_OUTPUTS_REGISTRY}" == "ghcr.io" ]]; then echo "username=${GITHUB_ACTOR}" >> "$GITHUB_OUTPUT" echo "password=${GITHUB_TOKEN}" >> "$GITHUB_OUTPUT" fi if [[ "${STEPS_SET_INPUTS_OUTPUTS_REGISTRY}" == "docker.io" ]]; then echo "username=meltano" >> "$GITHUB_OUTPUT" echo "password=${DOCKERHUB_TOKEN}" >> "$GITHUB_OUTPUT" fi env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} STEPS_SET_INPUTS_OUTPUTS_REGISTRY: ${{ steps.set-inputs.outputs.registry }} - name: Build, scan, then conditionally push the Docker image for a given Python version uses: ./.github/actions/docker-build-scan-push with: # Push to the container registry if # - The trigger is a push to the main branch # - The trigger is a workflow dispatch # - The trigger is a release and the action is 'published' # - The trigger is a weekly schedule (re-roll latest release with updated base image) push: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/main' ) || ( github.event_name == 'workflow_dispatch' ) || ( github.event_name == 'release' && github.event.action == 'published' ) || github.event_name == 'schedule' }} token: ${{ secrets.GITHUB_TOKEN }} tags: ${{ env.IMAGE_TAGS }} registry: ${{ steps.set-inputs.outputs.registry }} username: ${{ steps.user-and-pass.outputs.username }} password: ${{ steps.user-and-pass.outputs.password }} python-version: ${{ matrix.python-version }} slim: ${{ matrix.slim }} pypi_release: timeout-minutes: 30 name: Publish to PyPI if: github.event_name == 'release' && github.event.action == 'published' needs: [build, build_docker] runs-on: latchkey-small permissions: id-token: write # Needed for OIDC PyPI publishing environment: name: publishing url: https://pypi.org/project/${{ needs.build.outputs.name }}/${{ needs.build.outputs.version }} steps: - name: Check out the repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: Packages path: dist - name: Publish uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
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.
This workflow runs 3 jobs (12 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.