chore(ci): simplify release workflows — remove pretests, add commit picker and bump-only mode (#3177)

This commit is contained in:
Steven Enamakel
2026-06-01 21:09:26 -07:00
committed by GitHub
parent 6ba2ba2717
commit 47fa3f965b
4 changed files with 117 additions and 140 deletions
+1 -9
View File
@@ -84,14 +84,6 @@ on:
the Tauri shell since #1061).
type: boolean
default: false
skip_pretests:
description: Metadata-only flag propagated by caller workflows when they
intentionally bypass the upstream pretest phase. This reusable build
workflow does not execute pretests itself, but it logs the policy so
the build run captures whether the normal release gate was relaxed for
this invocation.
type: boolean
default: false
with_updater:
description:
When true, set `WITH_UPDATER=true` so the Tauri bundler emits the
@@ -146,7 +138,7 @@ jobs:
- name: Log build policy
shell: bash
run: |
echo "[build-desktop] tag=${{ inputs.tag }} build_ref=${{ inputs.build_ref }} skip_pretests=${{ inputs.skip_pretests }}"
echo "[build-desktop] tag=${{ inputs.tag }} build_ref=${{ inputs.build_ref }}"
- name: Set Xcode version
if: matrix.settings.platform == 'macos-latest'
uses: maxim-lobanov/setup-xcode@v1
+54 -74
View File
@@ -28,15 +28,15 @@ on:
default: patch
type: choice
options: [patch, minor, major]
skip_pretests:
commit_sha:
description:
Skip the unit/rust pretest phase and continue directly to the build
matrix. Release workflows no longer run E2E. Use only when the
required pretest signal is already known and you need to unblock a
production cut.
Build from a specific commit SHA instead of main HEAD. Only
consulted when release_source = main_head; staging_tag promotions
resolve the commit from the tag. The commit must be reachable
from main. Leave empty to use main HEAD.
required: false
type: boolean
default: false
type: string
default: ""
create_release:
description:
Create and publish the GitHub Release and attach release assets. When
@@ -58,8 +58,6 @@ concurrency:
#
# prepare-build
# │
# ├─── pretest-tests (reusable test-reusable.yml — unit + rust)
# │
# ├─── create-release (optional no-op when `create_release=false`)
# │ │
# │ ┌────┴───────────────┬────────────────┐
@@ -174,13 +172,28 @@ jobs:
- name: Configure Git
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
RELEASE_SOURCE: ${{ inputs.release_source }}
COMMIT_SHA: ${{ inputs.commit_sha }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin https://${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git fetch origin --tags --prune --prune-tags
git checkout main
git pull origin main --ff-only
if [ "$RELEASE_SOURCE" = "main_head" ] && [ -n "$COMMIT_SHA" ]; then
[[ "$COMMIT_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]] || {
echo "Invalid commit_sha: must be a hex SHA (7-40 chars)"
exit 1
}
git rev-parse --verify "${COMMIT_SHA}^{commit}" >/dev/null
git merge-base --is-ancestor "$COMMIT_SHA" origin/main || {
echo "commit_sha $COMMIT_SHA is not reachable from main"
exit 1
}
git checkout --detach "$COMMIT_SHA"
else
git checkout main
git pull origin main --ff-only
fi
# ── Path A: main_head ────────────────────────────────────────────────
# Bump version on main, commit, push, tag.
@@ -201,7 +214,7 @@ jobs:
cargo update --workspace --manifest-path Cargo.toml
cargo update --workspace --manifest-path app/src-tauri/Cargo.toml
- name: Ensure tag does not already exist (main_head)
if: inputs.release_source == 'main_head'
if: inputs.release_source == 'main_head' && inputs.create_release
env:
TAG: ${{ steps.bump.outputs.tag }}
run: |
@@ -213,18 +226,23 @@ jobs:
echo "Tag already exists on origin: $TAG"
exit 1
fi
- name: Commit, push and tag (main_head)
- name: Commit and push version bump (main_head)
if: inputs.release_source == 'main_head'
id: push
env:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.bump.outputs.tag }}
CREATE_RELEASE: ${{ inputs.create_release }}
run: |
git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml Cargo.toml app/src-tauri/Cargo.lock Cargo.lock
git commit -m "chore(release): v${VERSION}"
git push origin main
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
git push origin HEAD:main
if [ "$CREATE_RELEASE" = "true" ]; then
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
else
echo "Skipping tag creation (create_release=false)"
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# ── Path B: staging_tag ──────────────────────────────────────────────
@@ -295,7 +313,7 @@ jobs:
echo "prod_version=$PROD_VERSION" >> "$GITHUB_OUTPUT"
echo "prod_tag=$PROD_TAG" >> "$GITHUB_OUTPUT"
- name: Ensure production tag does not already exist (staging_tag)
if: inputs.release_source == 'staging_tag'
if: inputs.release_source == 'staging_tag' && inputs.create_release
env:
TAG: ${{ steps.stagingtag.outputs.prod_tag }}
run: |
@@ -308,7 +326,7 @@ jobs:
exit 1
fi
- name: Create production tag at staging commit (staging_tag)
if: inputs.release_source == 'staging_tag'
if: inputs.release_source == 'staging_tag' && inputs.create_release
id: promote
env:
STAGING_TAG: ${{ steps.stagingtag.outputs.staging_tag }}
@@ -324,12 +342,14 @@ jobs:
shell: bash
env:
RELEASE_SOURCE: ${{ inputs.release_source }}
CREATE_RELEASE: ${{ inputs.create_release }}
BUMP_VERSION: ${{ steps.bump.outputs.version }}
BUMP_TAG: ${{ steps.bump.outputs.tag }}
PUSH_SHA: ${{ steps.push.outputs.sha }}
PROMOTE_VERSION: ${{ steps.stagingtag.outputs.prod_version }}
PROMOTE_TAG: ${{ steps.stagingtag.outputs.prod_tag }}
PROMOTE_SHA: ${{ steps.promote.outputs.sha }}
STAGING_SHA: ${{ steps.stagingtag.outputs.staging_sha }}
run: |
if [ "$RELEASE_SOURCE" = "main_head" ]; then
VERSION="$BUMP_VERSION"
@@ -338,14 +358,16 @@ jobs:
else
VERSION="$PROMOTE_VERSION"
TAG="$PROMOTE_TAG"
SHA="$PROMOTE_SHA"
# When create_release=false on staging_tag path, promote step
# is skipped so PROMOTE_SHA is empty — fall back to staging SHA.
SHA="${PROMOTE_SHA:-$STAGING_SHA}"
fi
if [ "$CREATE_RELEASE" = "true" ]; then
BUILD_REF="$TAG"
else
BUILD_REF="$SHA"
fi
BUILD_REF="$TAG"
BASE_URL="https://api.tinyhumans.ai/"
# Match the 12-char truncation runtime code applies to
# VITE_BUILD_SHA / OPENHUMAN_BUILD_SHA when constructing the release
# tag at startup, so SENTRY_RELEASE assembled in CI agrees with
# the tag events emit.
SHORT_SHA="${SHA:0:12}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
@@ -355,31 +377,13 @@ jobs:
echo "base_url=$BASE_URL" >> "$GITHUB_OUTPUT"
# =========================================================================
# Phase 1b: Pretest gate — run unit + rust on the build ref before we spin up the release
# draft or any signed-build matrix. Pretest failures abort the workflow
# before `create-release` runs, so a busted commit never produces a
# half-finished GH Release that has to be cleaned up.
# =========================================================================
pretest-tests:
name: Pretest — unit + rust
needs: [prepare-build]
if: ${{ !inputs.skip_pretests }}
uses: ./.github/workflows/test-reusable.yml
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
# =========================================================================
# Phase 2: Create draft GitHub release
# =========================================================================
create-release:
name: Prepare GitHub release
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, pretest-tests]
if: >-
always()
&& needs.prepare-build.result == 'success'
&& (needs.pretest-tests.result == 'success'
|| (inputs.skip_pretests && needs.pretest-tests.result == 'skipped'))
needs: [prepare-build]
outputs:
release_id: ${{ steps.create.outputs.release_id || steps.noop.outputs.release_id }}
upload_url: ${{ steps.create.outputs.upload_url || steps.noop.outputs.upload_url }}
@@ -428,13 +432,6 @@ jobs:
build-desktop:
name: Build desktop matrix
needs: [prepare-build, create-release]
# `always()` is load-bearing: when `skip_pretests=true` the pretest job is
# `skipped`, and GitHub propagates that skipped status transitively to any
# downstream job lacking an explicit status function — even though we only
# `needs` create-release here, the build would otherwise be skipped along
# with the pretests. Same pattern below for build-cli-linux / build-docker /
# publish-updater-manifest / publish-release / tag-docker-latest /
# record-sentry-deploy. See release-staging.yml for the working precedent.
if: always() && needs.create-release.result == 'success'
uses: ./.github/workflows/build-desktop.yml
secrets: inherit
@@ -453,7 +450,6 @@ jobs:
with_release_upload: ${{ inputs.create_release }}
release_id: ${{ needs.create-release.outputs.release_id }}
build_sidecar: false
skip_pretests: ${{ inputs.skip_pretests }}
# =========================================================================
# Phase 3b: Build & push Docker image (runs parallel with build-desktop).
@@ -835,37 +831,22 @@ jobs:
environment: Production
needs:
- prepare-build
- pretest-tests
- create-release
- build-desktop
- build-cli-linux
- build-docker
- publish-updater-manifest
# Two cleanup paths:
# (a) pretest failed → no release yet, but `prepare-build` already
# pushed a tag and (for main_head) a chore(release) commit; we
# still need to delete the tag so a rerun doesn't trip the
# "tag already exists" guard.
# (b) any build phase failed after create-release succeeded — the
# original behavior — delete the draft release, the tag, and any
# pushed Docker image versions.
if: >-
always()
&& inputs.create_release
&& needs.prepare-build.result == 'success'
&& (
(needs.create-release.result != 'success'
&& (needs.pretest-tests.result == 'failure' || needs.pretest-tests.result == 'cancelled'
))
|| (needs.create-release.result == 'success'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|| needs.build-cli-linux.result == 'failure' || needs.build-cli-linux.result == 'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled'
|| needs.publish-updater-manifest.result == 'failure' || needs.publish-updater-manifest.result == 'cancelled'))
)
&& needs.create-release.result == 'success'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|| needs.build-cli-linux.result == 'failure' || needs.build-cli-linux.result == 'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled'
|| needs.publish-updater-manifest.result == 'failure' || needs.publish-updater-manifest.result == 'cancelled')
steps:
- name: Delete GitHub release
# Skip on the pretest-failure cleanup path: create-release didn't run,
# so there's no draft release to delete (only an orphaned tag).
if: ${{ inputs.create_release && needs.create-release.result == 'success' }}
uses: actions/github-script@v8
with:
@@ -906,8 +887,7 @@ jobs:
# `:v<version>` and `:<version>` tags we just pushed and remove the
# underlying package version. `:latest` is left alone — the previous
# release is still pointed at it, and clobbering the moving tag here
# would orphan downstream pulls. Skipped on the pretest-failure
# cleanup path where build-docker never ran.
# would orphan downstream pulls.
if: needs.build-docker.result == 'success'
continue-on-error: true
env:
+61 -55
View File
@@ -3,15 +3,23 @@ name: Release Staging
on:
workflow_dispatch:
inputs:
skip_pretests:
commit_sha:
description:
Skip the unit/rust pretest phase and continue directly to the
desktop/docker staging build. Release workflows no longer run E2E.
Use only when the required pretest signal is already known and you
need to unblock a staging cut.
Build from a specific commit SHA instead of main HEAD. Leave empty
to use main HEAD (the default). The commit must be reachable from
main.
required: false
type: string
default: ""
create_tag:
description:
Create the immutable `v<version>-staging` tag and run the full
build matrix. When false, bump versions and commit to main but
skip tag creation, desktop builds, and Docker — useful for
version-bump-only runs.
required: false
type: boolean
default: false
default: true
permissions:
# `contents: write` is required for the patch bump commit and the
# `v<version>-staging` tag push performed by `prepare-build` below.
@@ -25,20 +33,15 @@ concurrency:
#
# prepare-build
# │
# ├── pretest-tests (reusable test-reusable.yml — unit + rust;
# │ optional when `skip_pretests` is true)
# ├── pretest-tests (reusable test-reusable.yml — unit + rust)
# │
# ├── build-desktop (delegated to .github/workflows/build-desktop.yml)
# ├── build-docker (build only — no GHCR push on staging)
# ├── build-desktop (delegated to .github/workflows/build-desktop.yml;
# │ skipped when `create_tag=false`)
# ├── build-docker (build only — no GHCR push on staging;
# │ skipped when `create_tag=false`)
# │
# record-sentry-deploy
# │
# cleanup-failed-staging (on failure)
#
# The pretest job is a hard gate — `build-desktop` and `build-docker`
# only start once unit/rust have passed, unless explicitly skipped.
#
# The actual desktop build / Sentry / artifact-upload pipeline lives in
# `.github/workflows/build-desktop.yml` and is shared with
# release-production.yml.
@@ -105,13 +108,27 @@ jobs:
- name: Configure Git
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
COMMIT_SHA: ${{ inputs.commit_sha }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin https://${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git fetch origin --tags --prune --prune-tags
git checkout main
git pull origin main --ff-only
if [ -n "$COMMIT_SHA" ]; then
[[ "$COMMIT_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]] || {
echo "Invalid commit_sha: must be a hex SHA (7-40 chars)"
exit 1
}
git rev-parse --verify "${COMMIT_SHA}^{commit}" >/dev/null
git merge-base --is-ancestor "$COMMIT_SHA" origin/main || {
echo "commit_sha $COMMIT_SHA is not reachable from main"
exit 1
}
git checkout --detach "$COMMIT_SHA"
else
git checkout main
git pull origin main --ff-only
fi
# Patch-only bump for staging cuts. Minor/major promotions are owned
# by `release-production.yml` and only happen on the production path.
# Bump commit lands on `main` (we don't maintain a separate `staging`
@@ -139,6 +156,7 @@ jobs:
STAGING_TAG="v${VERSION}-staging"
echo "tag=${STAGING_TAG}" >> "$GITHUB_OUTPUT"
- name: Ensure staging tag does not already exist
if: inputs.create_tag
env:
TAG: ${{ steps.tagname.outputs.tag }}
run: |
@@ -150,17 +168,22 @@ jobs:
echo "Tag already exists on origin: $TAG"
exit 1
fi
- name: Commit, push and tag staging cut
- name: Commit and push version bump
id: push
env:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.tagname.outputs.tag }}
CREATE_TAG: ${{ inputs.create_tag }}
run: |
git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml Cargo.toml app/src-tauri/Cargo.lock Cargo.lock
git commit -m "chore(staging): v${VERSION}"
git push origin main
git tag -a "$TAG" -m "Staging cut $TAG"
git push origin "$TAG"
git push origin HEAD:main
if [ "$CREATE_TAG" = "true" ]; then
git tag -a "$TAG" -m "Staging cut $TAG"
git push origin "$TAG"
else
echo "Skipping tag creation (create_tag=false)"
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Resolve build outputs
id: resolve
@@ -169,44 +192,30 @@ jobs:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.tagname.outputs.tag }}
SHA: ${{ steps.push.outputs.sha }}
CREATE_TAG: ${{ inputs.create_tag }}
run: |
# Match the 12-char truncation runtime code applies to
# VITE_BUILD_SHA / OPENHUMAN_BUILD_SHA when constructing the
# release tag at startup, so SENTRY_RELEASE assembled in CI
# agrees with the tag events emit.
SHORT_SHA="${SHA:0:12}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
# Build from the immutable staging tag rather than main HEAD so
# reruns of this workflow rebuild the same content even if main
# has moved on (e.g. another patch cut, or a hotfix landed).
echo "build_ref=$TAG" >> "$GITHUB_OUTPUT"
if [ "$CREATE_TAG" = "true" ]; then
# Build from the immutable staging tag rather than main HEAD so
# reruns of this workflow rebuild the same content even if main
# has moved on (e.g. another patch cut, or a hotfix landed).
echo "build_ref=$TAG" >> "$GITHUB_OUTPUT"
else
echo "build_ref=$SHA" >> "$GITHUB_OUTPUT"
fi
echo "base_url=https://staging-api.tinyhumans.ai/" >> "$GITHUB_OUTPUT"
# =========================================================================
# Phase 1b: Pretest gate — run unit + rust once on the staging commit before any build job
# spins up. A failure here aborts the matrix (and `cleanup-failed-staging`
# deletes the tag) without burning four signed Tauri builds first.
# =========================================================================
pretest-tests:
name: Pretest — unit + rust
needs: [prepare-build]
if: ${{ !inputs.skip_pretests }}
uses: ./.github/workflows/test-reusable.yml
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
# =========================================================================
# Phase 2: Build desktop artifacts (delegated to reusable workflow)
# =========================================================================
build-desktop:
name: Build desktop matrix
needs: [prepare-build, pretest-tests]
if: >-
always()
&& (needs.pretest-tests.result == 'success'
|| (inputs.skip_pretests && needs.pretest-tests.result == 'skipped'))
needs: [prepare-build]
if: inputs.create_tag
uses: ./.github/workflows/build-desktop.yml
secrets: inherit
with:
@@ -236,7 +245,6 @@ jobs:
# real consumer. Set `build_sidecar: true` to re-enable a per-platform
# CLI Actions artifact + its Sentry DIF upload for QA spot-checks.
build_sidecar: false
skip_pretests: ${{ inputs.skip_pretests }}
# =========================================================================
# Phase 2b: Build the openhuman-core Docker image without pushing.
@@ -249,11 +257,8 @@ jobs:
# =========================================================================
build-docker:
name: "Docker: build (no push)"
needs: [prepare-build, pretest-tests]
if: >-
always()
&& (needs.pretest-tests.result == 'success'
|| (inputs.skip_pretests && needs.pretest-tests.result == 'skipped'))
needs: [prepare-build]
if: inputs.create_tag
runs-on: ubuntu-latest
environment: Production
steps:
@@ -294,6 +299,7 @@ jobs:
name: Record Sentry deploy marker
runs-on: ubuntu-latest
environment: Production
if: inputs.create_tag
needs: [prepare-build, build-desktop, build-docker]
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -334,12 +340,12 @@ jobs:
name: Remove staging tag if build failed
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, pretest-tests, build-desktop, build-docker]
needs: [prepare-build, build-desktop, build-docker]
if: >-
always()
&& inputs.create_tag
&& needs.prepare-build.result == 'success'
&& (needs.pretest-tests.result == 'failure' || needs.pretest-tests.result == 'cancelled'
|| needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled')
steps:
- name: Delete remote staging tag
+1 -2
View File
@@ -1,7 +1,6 @@
---
# Reusable test workflow — frontend unit tests, Rust core tests, Rust Tauri
# shell tests. Same job graph used by both PR/push (`test.yml`) and the
# release pretest gate (`release-staging.yml`, `release-production.yml`).
# shell tests. Used by PR/push (`test.yml`).
#
# Caching: pnpm store, Swatinem rust-cache, CEF runtime, and sccache backed
# by the GitHub Actions cache. Cache keys mirror what the release build