Skip to content

release: prepare Dairo CLI v0.0.8 #16

release: prepare Dairo CLI v0.0.8

release: prepare Dairo CLI v0.0.8 #16

Workflow file for this run

name: Release CLI
# Release tags are always v-prefixed (vX.Y.Z). The tag version must match
# Cargo.toml or the release job fails. npm publishing is launch-gated by the
# NPM_PUBLISH repo variable (plus the NPM_TOKEN secret); everything else is
# mandatory — a missing secret fails the release instead of silently skipping.
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to package, e.g. 0.1.0. Defaults to Cargo.toml version.'
required: false
type: string
publish_npm:
description: 'Publish npm packages (requires NPM_TOKEN).'
required: false
default: false
type: boolean
update_homebrew:
description: 'Update dairo-app/homebrew-tap (requires HOMEBREW_TAP_TOKEN).'
required: false
default: false
type: boolean
permissions:
contents: read
jobs:
build:
name: Build ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
target: aarch64-apple-darwin
exe: dairo
- os: macos-15-intel
target: x86_64-apple-darwin
exe: dairo
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
exe: dairo
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
exe: dairo
- os: windows-latest
target: x86_64-pc-windows-msvc
exe: dairo.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- run: cargo build --release --locked --target ${{ matrix.target }}
- name: Stage binary
shell: bash
run: |
set -euo pipefail
mkdir -p dist/binaries/${{ matrix.target }}
cp target/${{ matrix.target }}/release/${{ matrix.exe }} dist/binaries/${{ matrix.target }}/${{ matrix.exe }}
if [ "${{ runner.os }}" != "Windows" ]; then chmod +x dist/binaries/${{ matrix.target }}/${{ matrix.exe }}; fi
# Every runner in the matrix is native for its target, so the staged
# binary must run and report the Cargo.toml version before we ship it.
- name: Smoke test binary
shell: bash
run: |
set -euo pipefail
cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
output="$(dist/binaries/${{ matrix.target }}/${{ matrix.exe }} --version)"
echo "$output"
case "$output" in
*"$cargo_version"*) ;;
*)
echo "::error::--version reported '$output', expected Cargo.toml version $cargo_version" >&2
exit 1
;;
esac
- uses: actions/upload-artifact@v7
with:
name: binary-${{ matrix.target }}
path: dist/binaries/${{ matrix.target }}
if-no-files-found: error
release:
name: Create GitHub release artifacts
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
pattern: binary-*
path: dist/downloaded
- name: Normalize downloaded binaries
run: |
set -euo pipefail
mkdir -p dist/binaries
for dir in dist/downloaded/binary-*; do
target="${dir##*/binary-}"
mkdir -p "dist/binaries/$target"
cp -R "$dir"/* "dist/binaries/$target/"
done
# actions/download-artifact strips file modes; restore the execute
# bit so the published tarballs contain runnable binaries.
find dist/binaries -type f -name dairo -exec chmod 0755 {} +
- id: meta
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
elif [ -n "${INPUT_VERSION:-}" ]; then
VERSION="${INPUT_VERSION#v}"
TAG="v$VERSION"
else
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
TAG="v$VERSION"
fi
if ! printf '%s\n' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$'; then
echo "Invalid release version: $VERSION" >&2
exit 1
fi
CARGO_VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
if [ "$CARGO_VERSION" != "$VERSION" ]; then
echo "Cargo.toml version ($CARGO_VERSION) must match release tag version ($VERSION)." >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Create archives and checksums
run: |
set -euo pipefail
mkdir -p dist/release
for target_dir in dist/binaries/*; do
target="${target_dir##*/}"
if [ -f "$target_dir/dairo.exe" ]; then
archive="dairo-${target}.zip"
(cd "$target_dir" && zip -q "../../release/$archive" dairo.exe)
else
archive="dairo-${target}.tar.gz"
tar -C "$target_dir" -czf "dist/release/$archive" dairo
if ! tar -tvzf "dist/release/$archive" | grep -q '^-rwx'; then
echo "::error::$archive contains a non-executable dairo binary" >&2
exit 1
fi
fi
done
cp install/install.sh dist/release/dairo-install.sh
cp install/install.ps1 dist/release/dairo-install.ps1
(cd dist/release && shasum -a 256 dairo-* > checksums.txt)
- uses: actions/upload-artifact@v7
with:
name: release-assets
path: dist/release/*
- id: gcp-auth
name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.GCP_RELEASE_NOTES_SERVICE_ACCOUNT }}
token_format: access_token
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Generate release notes
env:
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
RELEASE_VERSION: ${{ steps.meta.outputs.version }}
RELEASE_NOTES_FILE: release-notes.md
GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
GCP_LOCATION: ${{ vars.GCP_LOCATION || 'eu' }}
VERTEX_MODEL: ${{ vars.VERTEX_MODEL || 'gemini-3.5-flash' }}
VERTEX_ACCESS_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
run: node scripts/generate-release-notes.mjs
- id: release-app-token
name: Create dairo-bot release token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: dairo-app
repositories: dairo-cli
permission-contents: write
- name: Publish GitHub release
env:
GH_TOKEN: ${{ steps.release-app-token.outputs.token }}
run: |
set -euo pipefail
TAG='${{ steps.meta.outputs.tag }}'
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" dist/release/* --clobber
gh release edit "$TAG" --title "Dairo CLI $TAG" --notes-file release-notes.md
else
gh release create "$TAG" dist/release/* \
--title "Dairo CLI $TAG" \
--notes-file release-notes.md
fi
npm:
name: Build npm packages
needs: [build, release]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
# Trusted publishing (OIDC) needs npm >= 11.5.1; node 24 ships it.
node-version: 24
registry-url: https://registry.npmjs.org
scope: '@dairo-app'
- name: Require an OIDC-capable npm
run: |
set -euo pipefail
npm_version="$(npm --version)"
if [ "$(printf '%s\n11.5.1\n' "$npm_version" | sort -V | head -1)" != "11.5.1" ]; then
echo "::error::npm $npm_version is too old for trusted publishing (need >= 11.5.1)." >&2
exit 1
fi
- uses: actions/download-artifact@v8
with:
pattern: binary-*
path: dist/downloaded
- name: Normalize downloaded binaries
run: |
set -euo pipefail
mkdir -p dist/binaries
for dir in dist/downloaded/binary-*; do
target="${dir##*/binary-}"
mkdir -p "dist/binaries/$target"
cp -R "$dir"/* "dist/binaries/$target/"
done
- name: Prepare npm packages
env:
DAIRO_CLI_VERSION: ${{ needs.release.outputs.version }}
run: node scripts/prepare-npm-packages.mjs
- name: Pack npm packages
run: |
set -euo pipefail
mkdir -p dist/npm-tarballs
for dir in dist/npm/*; do
(cd "$dir" && npm pack --pack-destination ../../npm-tarballs)
done
- uses: actions/upload-artifact@v7
with:
name: npm-packages
path: dist/npm-tarballs/*.tgz
# Launch gate: set the NPM_PUBLISH repo variable to 'true' to publish on
# tag pushes. Auth is trusted publishing (OIDC via id-token: write) — no
# npm token exists; every package must have this repo + workflow
# registered as a trusted publisher on npmjs.com, and provenance is
# attested automatically.
- name: Publish npm packages
if: ${{ (github.event_name == 'push' && vars.NPM_PUBLISH == 'true') || (github.event_name == 'workflow_dispatch' && inputs.publish_npm) }}
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
set -euo pipefail
for pkg in cli-darwin-arm64 cli-darwin-x64 cli-linux-x64 cli-linux-arm64 cli-win32-x64 cli dairo-cli; do
if [ "$pkg" = "dairo-cli" ]; then
name="dairo-cli"
else
name="@dairo-app/$pkg"
fi
if [ "$(npm view "$name@$VERSION" version 2>/dev/null || true)" = "$VERSION" ]; then
echo "$name@$VERSION is already published; leaving it untouched."
continue
fi
npm publish "dist/npm/$pkg" --access public
done
homebrew:
name: Update Homebrew tap
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
if: ${{ github.event_name == 'push' || inputs.update_homebrew }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: release-assets
path: dist/release
- name: Update formula in dairo-app/homebrew-tap
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
TAG: ${{ needs.release.outputs.tag }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::HOMEBREW_TAP_TOKEN is not configured; the Homebrew tap cannot be updated." >&2
exit 1
fi
gh repo clone dairo-app/homebrew-tap tap
cd tap
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/dairo-app/homebrew-tap.git"
mkdir -p Formula
python3 ../scripts/render-homebrew-formula.py "$VERSION" "$TAG" ../dist/release/checksums.txt > Formula/dairo.rb
git config user.name "dairo-release-bot"
git config user.email "release-bot@dairo.app"
git add Formula/dairo.rb
if git diff --cached --quiet; then
echo "Formula already up to date for $TAG."
else
git commit -m "Update Dairo CLI to $TAG"
git push
fi