Skip to content

feat(release): unify product versioning #7

feat(release): unify product versioning

feat(release): unify product versioning #7

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
pull_request:
paths:
- ".github/workflows/release.yml"
- ".github/scripts/**"
- "apps/headless/Dockerfile.linux"
- "apps/headless/build.sh"
- "apps/headless/build-linux.sh"
- "apps/headless/install-linux.sh"
- "apps/headless/Package.swift"
- "apps/headless/Sources/HeadlessProtocol/ProductVersion.swift"
- "apps/headless/VERSION"
- "apps/headless/VersionSupport/**"
workflow_dispatch:
inputs:
dry_run:
description: Build and verify release packages without publishing
required: true
default: true
type: boolean
permissions:
contents: read
jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
publish: ${{ steps.version.outputs.publish }}
steps:
- uses: actions/checkout@v7
- id: version
shell: bash
env:
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
version="${GITHUB_REF_NAME#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a semantic version prefixed with v: $GITHUB_REF_NAME" >&2
exit 64
fi
expected="$(tr -d '[:space:]' < apps/headless/VERSION)"
if [[ "$version" != "$expected" ]]; then
echo "Release tag $version does not match apps/headless/VERSION $expected" >&2
exit 64
fi
echo "publish=true" >> "$GITHUB_OUTPUT"
else
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$DRY_RUN" != "true" ]]; then
echo "manual release runs must use dry_run=true; publish by pushing a v* tag" >&2
exit 64
fi
version="0.0.${GITHUB_RUN_NUMBER}"
echo "publish=false" >> "$GITHUB_OUTPUT"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
macos:
needs: version
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Install FFmpeg
run: brew install ffmpeg
- name: Build
env:
HEADLESS_VERSION: ${{ needs.version.outputs.version }}
run: ./apps/headless/build.sh
- name: Unit tests
env:
HEADLESS_VERSION: ${{ needs.version.outputs.version }}
run: ./apps/headless/test.sh
- name: E2E
run: zsh ./apps/headless/Tests/macos-e2e.sh
- name: Package
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -eu
cd apps/headless
ditto -c -k --keepParent Headless.app "Headless-${VERSION}-macos.zip"
test -s "Headless-${VERSION}-macos.zip"
unzip -t "Headless-${VERSION}-macos.zip"
unzip -Z1 "Headless-${VERSION}-macos.zip" | grep -qx 'Headless.app/Contents/MacOS/Headless'
unzip -Z1 "Headless-${VERSION}-macos.zip" | grep -qx 'Headless.app/Contents/Resources/bin/headless'
unzip -Z1 "Headless-${VERSION}-macos.zip" | grep -qx 'Headless.app/Contents/Resources/bin/headless-mcp'
- uses: actions/upload-artifact@v7
with:
name: macos
path: apps/headless/Headless-${{ needs.version.outputs.version }}-macos.zip
linux-amd64:
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Build
env:
HEADLESS_LINUX_PLATFORM: linux/amd64
HEADLESS_VERSION: ${{ needs.version.outputs.version }}
run: ./apps/headless/build-linux.sh
- name: E2E
run: ./apps/headless/Tests/linux-docker.sh
- name: Package
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -eu
cp apps/headless/build/headless-linux-amd64.tar.gz \
"apps/headless/build/headless-${VERSION}-linux-amd64.tar.gz"
archive="apps/headless/build/headless-${VERSION}-linux-amd64.tar.gz"
test -s "$archive"
tar -tzf "$archive" > archive-contents.txt
for expected in headless headless-host headless-mcp install-linux.sh Headless_HeadlessProtocol.resources/AgentRuntime.js; do
grep -qx "$expected" archive-contents.txt
done
- uses: actions/upload-artifact@v7
with:
name: linux-amd64
path: apps/headless/build/headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz
linux-arm64:
needs: version
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Build
env:
HEADLESS_LINUX_PLATFORM: linux/arm64
HEADLESS_VERSION: ${{ needs.version.outputs.version }}
run: ./apps/headless/build-linux.sh
- name: E2E
run: ./apps/headless/Tests/linux-docker.sh
- name: Package
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -eu
cp apps/headless/build/headless-linux-arm64.tar.gz \
"apps/headless/build/headless-${VERSION}-linux-arm64.tar.gz"
archive="apps/headless/build/headless-${VERSION}-linux-arm64.tar.gz"
test -s "$archive"
tar -tzf "$archive" > archive-contents.txt
for expected in headless headless-host headless-mcp install-linux.sh Headless_HeadlessProtocol.resources/AgentRuntime.js; do
grep -qx "$expected" archive-contents.txt
done
- uses: actions/upload-artifact@v7
with:
name: linux-arm64
path: apps/headless/build/headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz
publish:
needs: [version, macos, linux-amd64, linux-arm64]
if: >-
github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/v') &&
needs.version.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Generate and verify checksums
run: ./.github/scripts/release-checksums.sh "${{ needs.version.outputs.version }}" dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: Headless ${{ needs.version.outputs.version }}
generate_release_notes: true
body: |
## Downloads
| File | Platform |
| --- | --- |
| `Headless-${{ needs.version.outputs.version }}-macos.zip` | macOS (ad-hoc signed `.app`) |
| `headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz` | Linux x86_64 |
| `headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz` | Linux arm64 |
### Notes
- **macOS:** Unzip and run `Headless.app`. Gatekeeper may warn (ad-hoc signature; notarization not included yet). CLI: `Headless.app/Contents/Resources/bin/headless`.
- **Linux tarball:** Does not bundle Chromium. Install with `./install-linux.sh` after ensuring non-Snap Chromium and FFmpeg are available. The Docker production image remains the self-contained runtime (not published by this release).
files: |
dist/Headless-${{ needs.version.outputs.version }}-macos.zip
dist/headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz
dist/headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz
dist/SHA256SUMS
fail_on_unmatched_files: true