mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
chore(ci): remove staging_tag promotion path from release-production (#3179)
This commit is contained in:
@@ -3,37 +3,16 @@ name: Release Production
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_source:
|
||||
description: |
|
||||
Source ref for the production build.
|
||||
- staging_tag: build the latest (or explicit) staging tag — recommended; the artifact
|
||||
QA already exercised gets promoted to production.
|
||||
- main_head: build main @ HEAD with a fresh version bump (escape hatch for hotfixes).
|
||||
required: true
|
||||
type: choice
|
||||
default: staging_tag
|
||||
options: [staging_tag, main_head]
|
||||
staging_tag:
|
||||
description:
|
||||
Specific staging tag to promote (e.g. v1.2.4-staging). Leave empty to use the
|
||||
latest matching tag. Ignored when release_source = main_head.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
release_type:
|
||||
description:
|
||||
Version increment type. Only consulted when release_source = main_head;
|
||||
staging_tag promotions inherit the staging tag's version verbatim.
|
||||
description: Version increment type for the production release.
|
||||
required: false
|
||||
default: patch
|
||||
type: choice
|
||||
options: [patch, minor, major]
|
||||
commit_sha:
|
||||
description:
|
||||
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.
|
||||
Build from a specific commit SHA instead of main HEAD. The commit
|
||||
must be reachable from main. Leave empty to use main HEAD.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
@@ -49,8 +28,8 @@ permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
concurrency:
|
||||
# Distinct group from release-staging.yml so promotions and staging cuts can
|
||||
# run independently. The two workflows never touch the same tags or refs.
|
||||
# Distinct group from release-staging.yml so production and staging cuts can
|
||||
# run independently.
|
||||
group: release-production
|
||||
cancel-in-progress: false
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -80,52 +59,34 @@ concurrency:
|
||||
# ---------------------------------------------------------------------------
|
||||
jobs:
|
||||
# =========================================================================
|
||||
# Phase 0: Manual approval gate (main_head only).
|
||||
# Phase 0: Manual approval gate.
|
||||
#
|
||||
# The `main_head` path bumps the version, COMMITS to `main`, and pushes that
|
||||
# commit + tag using a GitHub App token that bypasses branch protection
|
||||
# (prepare-build → "Commit, push and tag (main_head)"). If that App private
|
||||
# key (secrets.XGITHUB_APP_PRIVATE_KEY) ever leaked, an attacker could push
|
||||
# arbitrary commits to the default branch — CWE-250. This job parks the run
|
||||
# on the `Release-Approval` environment (configured with required reviewers
|
||||
# in repo settings) so a human must explicitly approve before prepare-build
|
||||
# is allowed to run its push step. A rejected/cancelled approval leaves
|
||||
# prepare-build skipped, so nothing is ever pushed.
|
||||
#
|
||||
# Scoped to `main_head` only: the `staging_tag` promotion path pushes just an
|
||||
# immutable `v<version>` tag at an already-validated staging commit — it does
|
||||
# NOT push a commit to `main` — so it stays gated only by the existing
|
||||
# `Production` environment and is intentionally skipped here (no regression).
|
||||
# This workflow bumps the version, COMMITS to `main`, and pushes that
|
||||
# commit + tag using a GitHub App token that bypasses branch protection.
|
||||
# If that App private key (secrets.XGITHUB_APP_PRIVATE_KEY) ever leaked,
|
||||
# an attacker could push arbitrary commits to the default branch — CWE-250.
|
||||
# This job parks the run on the `Release-Approval` environment (configured
|
||||
# with required reviewers in repo settings) so a human must explicitly
|
||||
# approve before prepare-build is allowed to run its push step.
|
||||
# =========================================================================
|
||||
review-approval:
|
||||
name: Manual approval gate (main_head)
|
||||
name: Manual approval gate
|
||||
runs-on: ubuntu-latest
|
||||
environment: Release-Approval
|
||||
if: inputs.release_source == 'main_head'
|
||||
steps:
|
||||
- name: Record approval
|
||||
run: |
|
||||
echo "[release-production] main_head push to main approved by a required reviewer."
|
||||
echo "[release-production] Push to main approved by a required reviewer."
|
||||
echo "Proceeding to version bump + commit/tag push on prepare-build."
|
||||
|
||||
# =========================================================================
|
||||
# Phase 1: Resolve build ref and (for main_head) bump version + create tag
|
||||
# Phase 1: Bump version on main, commit, push, and optionally tag
|
||||
# =========================================================================
|
||||
prepare-build:
|
||||
name: Prepare build context
|
||||
runs-on: ubuntu-latest
|
||||
environment: Production
|
||||
needs: [review-approval]
|
||||
# The push-to-main step lives inside this job (main_head path), so gating
|
||||
# prepare-build on review-approval gates the push. `!cancelled()` (not
|
||||
# `always()`) so a cancelled run never pushes; the success/skipped guard
|
||||
# lets the `staging_tag` path through (review-approval is skipped there)
|
||||
# while requiring an explicit approval success for `main_head`. A rejected
|
||||
# approval yields result == 'failure', failing the guard → no push.
|
||||
if: >-
|
||||
!cancelled()
|
||||
&& (needs.review-approval.result == 'success'
|
||||
|| needs.review-approval.result == 'skipped')
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
@@ -141,10 +102,6 @@ jobs:
|
||||
base_url: ${{ steps.resolve.outputs.base_url }}
|
||||
steps:
|
||||
- name: Enforce main branch
|
||||
# Both flows operate against main: main_head bumps and tags from main
|
||||
# HEAD; staging_tag promotion creates the production tag from main
|
||||
# (the tag's commit object lives in the same repo, so the tag is
|
||||
# reachable via direct ref regardless of where it sits in history).
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: |
|
||||
echo "This workflow can only run from main. Current ref: $GITHUB_REF"
|
||||
@@ -172,14 +129,13 @@ 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
|
||||
if [ "$RELEASE_SOURCE" = "main_head" ] && [ -n "$COMMIT_SHA" ]; then
|
||||
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
|
||||
@@ -194,27 +150,17 @@ jobs:
|
||||
git checkout main
|
||||
git pull origin main --ff-only
|
||||
fi
|
||||
|
||||
# ── Path A: main_head ────────────────────────────────────────────────
|
||||
# Bump version on main, commit, push, tag.
|
||||
- name: Compute next version and sync release files (main_head)
|
||||
if: inputs.release_source == 'main_head'
|
||||
- name: Compute next version and sync release files
|
||||
id: bump
|
||||
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
|
||||
- name: Verify release version sync (main_head)
|
||||
if: inputs.release_source == 'main_head'
|
||||
- name: Verify release version sync
|
||||
run: node scripts/release/verify-version-sync.js "${{ steps.bump.outputs.version }}"
|
||||
- name: Refresh Cargo.lock files (main_head)
|
||||
if: inputs.release_source == 'main_head'
|
||||
# Cargo.toml [package].version is bumped above but the matching
|
||||
# entries in both Cargo.lock files are not — without this step the
|
||||
# lockfiles stay pinned to the previous version and the next local
|
||||
# `cargo` invocation rewrites them, leaving an uncommitted diff.
|
||||
- name: Refresh Cargo.lock files
|
||||
run: |
|
||||
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' && inputs.create_release
|
||||
- name: Ensure tag does not already exist
|
||||
if: inputs.create_release
|
||||
env:
|
||||
TAG: ${{ steps.bump.outputs.tag }}
|
||||
run: |
|
||||
@@ -226,8 +172,7 @@ jobs:
|
||||
echo "Tag already exists on origin: $TAG"
|
||||
exit 1
|
||||
fi
|
||||
- name: Commit and push version bump (main_head)
|
||||
if: inputs.release_source == 'main_head'
|
||||
- name: Commit and push version bump
|
||||
id: push
|
||||
env:
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
@@ -245,136 +190,27 @@ jobs:
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Path B: staging_tag ──────────────────────────────────────────────
|
||||
# Resolve a previously cut staging tag, derive the production version
|
||||
# from its package.json (no further bump — patch was applied during the
|
||||
# staging cut), and create the production v<version> tag at the same
|
||||
# commit. The artifact contents are byte-identical to what staging
|
||||
# validated, modulo build-time env (BASE_URL, OPENHUMAN_APP_ENV, etc.).
|
||||
- name: Resolve staging tag (staging_tag)
|
||||
if: inputs.release_source == 'staging_tag'
|
||||
id: stagingtag
|
||||
env:
|
||||
EXPLICIT_TAG: ${{ inputs.staging_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$EXPLICIT_TAG" ]; then
|
||||
STAGING_TAG="$EXPLICIT_TAG"
|
||||
else
|
||||
STAGING_TAG="$(git tag -l 'v*-staging' --sort=-v:refname | head -n 1)"
|
||||
fi
|
||||
if [ -z "$STAGING_TAG" ]; then
|
||||
echo "No staging tags found matching v*-staging."
|
||||
exit 1
|
||||
fi
|
||||
# Reject anything that isn't a `vX.Y.Z-staging` tag we cut
|
||||
# ourselves — keeps an operator from accidentally promoting a
|
||||
# hand-pushed ref or a stray pre-release tag.
|
||||
if ! [[ "$STAGING_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-staging$ ]]; then
|
||||
echo "Invalid staging tag format: $STAGING_TAG (expected vX.Y.Z-staging)"
|
||||
exit 1
|
||||
fi
|
||||
if ! git rev-parse --verify "refs/tags/$STAGING_TAG" >/dev/null 2>&1; then
|
||||
echo "Staging tag not present locally: $STAGING_TAG"
|
||||
exit 1
|
||||
fi
|
||||
STAGING_SHA="$(git rev-list -n 1 "$STAGING_TAG")"
|
||||
# Strip the -staging suffix to get the production version.
|
||||
PROD_VERSION="${STAGING_TAG#v}"
|
||||
PROD_VERSION="${PROD_VERSION%-staging}"
|
||||
PROD_TAG="v${PROD_VERSION}"
|
||||
# Sanity-check every authoritative version source on the staging
|
||||
# commit. They must all agree with PROD_VERSION — if they drift,
|
||||
# the bundled installer reports a different version than the
|
||||
# GitHub Release tag, and the Sentry release tag stops matching
|
||||
# what the running binary emits.
|
||||
read_json_version() {
|
||||
git show "${STAGING_TAG}:$1" | node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write(JSON.parse(d).version||""))'
|
||||
}
|
||||
read_cargo_version() {
|
||||
git show "${STAGING_TAG}:$1" | sed -n '/^\[package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p; }' | head -n 1
|
||||
}
|
||||
for src in \
|
||||
"app/package.json" \
|
||||
"app/src-tauri/tauri.conf.json" \
|
||||
"app/src-tauri/Cargo.toml" \
|
||||
"Cargo.toml"; do
|
||||
case "$src" in
|
||||
*.json) actual="$(read_json_version "$src")" ;;
|
||||
*.toml) actual="$(read_cargo_version "$src")" ;;
|
||||
esac
|
||||
if [ "$actual" != "$PROD_VERSION" ]; then
|
||||
echo "Staging tag $STAGING_TAG version mismatch: $src reports '$actual' but tag implies '$PROD_VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "staging_tag=$STAGING_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "staging_sha=$STAGING_SHA" >> "$GITHUB_OUTPUT"
|
||||
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' && inputs.create_release
|
||||
env:
|
||||
TAG: ${{ steps.stagingtag.outputs.prod_tag }}
|
||||
run: |
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag already exists locally: $TAG"
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
|
||||
echo "Tag already exists on origin: $TAG"
|
||||
exit 1
|
||||
fi
|
||||
- name: Create production tag at staging commit (staging_tag)
|
||||
if: inputs.release_source == 'staging_tag' && inputs.create_release
|
||||
id: promote
|
||||
env:
|
||||
STAGING_TAG: ${{ steps.stagingtag.outputs.staging_tag }}
|
||||
STAGING_SHA: ${{ steps.stagingtag.outputs.staging_sha }}
|
||||
PROD_TAG: ${{ steps.stagingtag.outputs.prod_tag }}
|
||||
run: |
|
||||
git tag -a "$PROD_TAG" "$STAGING_SHA" -m "Release $PROD_TAG (promoted from $STAGING_TAG)"
|
||||
git push origin "$PROD_TAG"
|
||||
echo "sha=$STAGING_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve build outputs
|
||||
id: resolve
|
||||
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 }}
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
TAG: ${{ steps.bump.outputs.tag }}
|
||||
SHA: ${{ steps.push.outputs.sha }}
|
||||
run: |
|
||||
if [ "$RELEASE_SOURCE" = "main_head" ]; then
|
||||
VERSION="$BUMP_VERSION"
|
||||
TAG="$BUMP_TAG"
|
||||
SHA="$PUSH_SHA"
|
||||
else
|
||||
VERSION="$PROMOTE_VERSION"
|
||||
TAG="$PROMOTE_TAG"
|
||||
# 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
|
||||
BASE_URL="https://api.tinyhumans.ai/"
|
||||
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"
|
||||
echo "build_ref=$BUILD_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "base_url=$BASE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "base_url=https://api.tinyhumans.ai/" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# =========================================================================
|
||||
# Phase 2: Create draft GitHub release
|
||||
|
||||
@@ -48,12 +48,9 @@ concurrency:
|
||||
# ---------------------------------------------------------------------------
|
||||
jobs:
|
||||
# =========================================================================
|
||||
# Phase 1: Patch-bump on `main` and create the immutable
|
||||
# Phase 1: Patch-bump on `main` and optionally create the immutable
|
||||
# `v<version>-staging` tag at that commit. The build matrix below then
|
||||
# checks out the tag (not main HEAD) so reruns reproduce byte-for-byte.
|
||||
# Production promotion (`release-production.yml`,
|
||||
# `release_source = staging_tag`) reads this tag verbatim and creates a
|
||||
# `v<version>` tag at the same commit.
|
||||
# =========================================================================
|
||||
prepare-build:
|
||||
name: Prepare build context
|
||||
|
||||
Reference in New Issue
Block a user