mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
ci: two-lane CI (ci-lite/ci-full), manual main→release promotion, universal back-merge (#4533)
This commit is contained in:
@@ -24,7 +24,7 @@ Suspected cause, workaround, or proposed fix. Skip if unknown.
|
||||
|
||||
- [ ] **Repro gone** — Bug no longer reproduces on the stated environment (or root cause documented if intentional).
|
||||
- [ ] **Regression safety** — Unit, integration, or E2E coverage added or updated if this should not come back.
|
||||
- [ ] **Diff coverage ≥ 80%** — the fix PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/pr-ci.yml`](../../.github/workflows/pr-ci.yml)).
|
||||
- [ ] **Diff coverage ≥ 80%** — the fix PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/ci-lite.yml`](../../.github/workflows/ci-lite.yml)).
|
||||
- [ ] **…** — Other verify-before-close items.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -25,7 +25,7 @@ How you plan to solve it — scope (core / app / both), approach, tradeoffs. Ski
|
||||
- [ ] **Feature 1** — TODO
|
||||
- [ ] **Feature 2** — TODO
|
||||
- [ ] **Feature 3** — TODO
|
||||
- [ ] **Diff coverage ≥ 80%** — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/pr-ci.yml`](../../.github/workflows/pr-ci.yml)).
|
||||
- [ ] **Diff coverage ≥ 80%** — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/ci-lite.yml`](../../.github/workflows/ci-lite.yml)).
|
||||
|
||||
- …
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ What is in scope, what is not, and any implementation notes or tradeoffs worth c
|
||||
- [ ] **Task 1** — TODO
|
||||
- [ ] **Task 2** — TODO
|
||||
- [ ] **Task 3** — TODO
|
||||
- [ ] **Diff coverage ≥ 80%** — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/pr-ci.yml`](../../.github/workflows/pr-ci.yml)) when code changes are involved.
|
||||
- [ ] **Diff coverage ≥ 80%** — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by [`.github/workflows/ci-lite.yml`](../../.github/workflows/ci-lite.yml)) when code changes are involved.
|
||||
|
||||
- …
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
> If a section does not apply to this change, mark the item as `N/A` with a one-line reason. Do not delete items.
|
||||
|
||||
- [ ] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement)
|
||||
- [ ] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/pr-ci.yml`](../.github/workflows/pr-ci.yml). Run `pnpm test:coverage` and `pnpm test:rust` locally; PRs below 80% on changed lines will not merge.
|
||||
- [ ] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/ci-lite.yml`](../.github/workflows/ci-lite.yml). Run `pnpm test:coverage` and `pnpm test:rust` locally; PRs below 80% on changed lines will not merge.
|
||||
- [ ] Coverage matrix updated — added/removed/renamed feature rows in [`docs/TEST-COVERAGE-MATRIX.md`](../docs/TEST-COVERAGE-MATRIX.md) reflect this change (or `N/A: behaviour-only change`)
|
||||
- [ ] All affected feature IDs from the matrix are listed in the PR description under `## Related`
|
||||
- [ ] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy))
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
---
|
||||
# Release CI — full-suite gate for PRs targeting the long-lived `release`
|
||||
# branch (normally the auto-prepared main→release PR).
|
||||
# CI Full — the slow lane. Runs EVERYTHING against the long-lived `release`
|
||||
# branch: full frontend + Rust unit suites, Rust mock-backend E2E, Playwright
|
||||
# web E2E, and the full desktop E2E matrix on Linux/macOS/Windows.
|
||||
#
|
||||
# Two-branch model:
|
||||
# - PRs → main run the fast lane (pr-ci.yml): quality checks + unit tests
|
||||
# scoped to the changed files only.
|
||||
# - main → release PRs run EVERYTHING here: full frontend + Rust unit
|
||||
# suites, Rust mock-backend E2E, Playwright web E2E, and the full desktop
|
||||
# E2E matrix on Linux/macOS/Windows. Maintainers can push fixups directly
|
||||
# to the PR until it greens up; releases are then cut from `release`.
|
||||
# Two-lane model:
|
||||
# - ci-lite.yml (quick): pushes to main + PRs to main/release — quality
|
||||
# checks + unit tests scoped to the changed files only.
|
||||
# - ci-full.yml (this file, slow): PRs targeting `release` (fix PRs opened
|
||||
# while stabilising a cut — the "CI Full Gate" check gates their merge)
|
||||
# and pushes to `release` (the maintainer-dispatched promotion merge from
|
||||
# promote-main-to-release.yml, which has no PR, plus each fix-PR merge
|
||||
# commit). Once the gate is green on release HEAD a release is cut via
|
||||
# release-staging.yml / release-production.yml.
|
||||
#
|
||||
# Branch protection on `release` requires the "Release CI Gate" check below.
|
||||
name: Release CI
|
||||
# The release workflows' version-bump commits carry [skip ci] so cutting a
|
||||
# build does not re-trigger this suite on an already-validated tree.
|
||||
name: CI Full
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
pull_request:
|
||||
branches: [release]
|
||||
workflow_dispatch: {}
|
||||
@@ -25,15 +31,16 @@ permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
# Consumed by scripts/ci-cancel-aware.sh's cancellation watchdog.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NPM_CONFIG_STORE_DIR: .pnpm-store
|
||||
|
||||
# Keep only the newest run per PR (or per ref for release pushes); the gate
|
||||
# result is per-commit, so cancelling the superseded run loses nothing.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -46,8 +53,8 @@ jobs:
|
||||
# pretest gate uses.
|
||||
unit-tests:
|
||||
name: Full Unit Suites
|
||||
# No `secrets: inherit` — the called workflow uses no secrets, and this
|
||||
# workflow is pull_request-triggered; keep its runs secret-free.
|
||||
# No `secrets: inherit` — the called workflow uses no secrets; keep this
|
||||
# workflow's runs secret-free.
|
||||
uses: ./.github/workflows/test-reusable.yml
|
||||
with:
|
||||
run_unit: true
|
||||
@@ -95,7 +102,7 @@ jobs:
|
||||
with:
|
||||
workspaces: . -> target
|
||||
cache-on-failure: true
|
||||
# Shared with pr-ci's former key so warm caches carry over between
|
||||
# Shared with ci-lite's former key so warm caches carry over between
|
||||
# the fast lane and this full lane on the same dependency tree.
|
||||
shared-key: pr-rust-core
|
||||
|
||||
@@ -294,8 +301,8 @@ jobs:
|
||||
# shards — see e2e-reusable.yml).
|
||||
e2e-desktop:
|
||||
name: Desktop E2E (full suite, 3 OS)
|
||||
# No `secrets: inherit` — the called workflow uses no secrets, and this
|
||||
# workflow is pull_request-triggered; keep its runs secret-free.
|
||||
# No `secrets: inherit` — the called workflow uses no secrets; keep this
|
||||
# workflow's runs secret-free.
|
||||
uses: ./.github/workflows/e2e-reusable.yml
|
||||
with:
|
||||
run_linux: true
|
||||
@@ -303,8 +310,8 @@ jobs:
|
||||
run_windows: true
|
||||
full: true
|
||||
|
||||
release-ci-gate:
|
||||
name: Release CI Gate
|
||||
ci-full-gate:
|
||||
name: CI Full Gate
|
||||
needs:
|
||||
- unit-tests
|
||||
- rust-e2e
|
||||
@@ -322,7 +329,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Require all Release CI jobs to pass
|
||||
- name: Require all CI Full jobs to pass
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# NOTE: "Playwright E2E" is deliberately absent — see the needs:
|
||||
@@ -1,17 +1,24 @@
|
||||
---
|
||||
# PR fast lane (PRs targeting main + pushes to main).
|
||||
# CI Lite — the quick lane (pushes to main + PRs targeting main or release).
|
||||
#
|
||||
# Runs quality checks per changed area and unit tests ONLY for the files the
|
||||
# PR changed (vitest related / domain-scoped cargo-llvm-cov), with diff-cover
|
||||
# enforcing >= 80% coverage on changed lines. Integration and E2E suites do
|
||||
# NOT run here — they run in full on main→release PRs via release-ci.yml.
|
||||
name: PR CI
|
||||
# NOT run here — they run in full via ci-full.yml on PRs targeting `release`
|
||||
# and on every push to `release` (the maintainer-dispatched
|
||||
# promote-main-to-release.yml merge and fix-PR merges). Fix PRs against
|
||||
# release get this fast lane too, for quick lint/coverage feedback alongside
|
||||
# the full suite.
|
||||
#
|
||||
# The gate job keeps its historical "PR CI Gate" check name so the main
|
||||
# ruleset's required-status-check reference stays valid.
|
||||
name: CI Lite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [main, release]
|
||||
|
||||
permissions:
|
||||
# `actions: read` lets scripts/ci-cancel-aware.sh poll the run status so
|
||||
@@ -77,7 +84,7 @@ jobs:
|
||||
list-files: shell
|
||||
filters: |
|
||||
frontend:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
# The changed-files runner itself must trigger the lane that
|
||||
# runs it (it is also in frontend-full → full-suite mode).
|
||||
- 'scripts/ci/vitest-changed-coverage.sh'
|
||||
@@ -95,7 +102,7 @@ jobs:
|
||||
- 'app/postcss.config.*'
|
||||
# Changes here invalidate per-file test scoping → full Vitest run.
|
||||
frontend-full:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'scripts/ci/vitest-changed-coverage.sh'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
@@ -112,17 +119,17 @@ jobs:
|
||||
- 'app/src/**/*.ts'
|
||||
- 'app/src/**/*.tsx'
|
||||
i18n:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'app/src/**'
|
||||
docs:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'app/src/App.tsx'
|
||||
- 'scripts/generate-architecture-docs.mjs'
|
||||
- 'scripts/__tests__/generate-architecture-docs.test.mjs'
|
||||
- 'gitbooks/developing/architecture/frontend.md'
|
||||
- 'package.json'
|
||||
rust-core:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
# The changed-files runner itself must trigger the lane that
|
||||
# runs it (it is also in rust-core-full → full-suite mode).
|
||||
- 'scripts/ci/rust-coverage-changed.sh'
|
||||
@@ -136,7 +143,7 @@ jobs:
|
||||
- 'scripts/test-rust-with-mock.sh'
|
||||
# Changes here invalidate per-module test scoping → full suite.
|
||||
rust-core-full:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'scripts/ci/rust-coverage-changed.sh'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
@@ -146,7 +153,7 @@ jobs:
|
||||
- 'src/**'
|
||||
- 'tests/**'
|
||||
rust-tauri:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'Cargo.lock'
|
||||
- 'rust-toolchain.toml'
|
||||
- 'app/src-tauri/**'
|
||||
@@ -156,7 +163,7 @@ jobs:
|
||||
# own socket-auth tests never ran in CI) — arm the scripts
|
||||
# self-test job on any mock/script change.
|
||||
scripts:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'scripts/mock-api/**'
|
||||
- 'scripts/*.mjs'
|
||||
- 'scripts/__tests__/**'
|
||||
@@ -172,7 +179,7 @@ jobs:
|
||||
- 'scripts/generate-test-inventory.mjs'
|
||||
# Windows installer + its Pester unit test.
|
||||
install_ps1:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- '.github/workflows/ci-lite.yml'
|
||||
- 'scripts/install.ps1'
|
||||
- 'scripts/tests/OpenHumanWindowsInstall.Tests.ps1'
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
# Keeps the main→release PR standing at all times.
|
||||
#
|
||||
# Every push to main ensures there is exactly one open PR from main into the
|
||||
# long-lived `release` branch. GitHub automatically refreshes an open
|
||||
# branch-to-branch PR as main advances, so if one already exists this is a
|
||||
# no-op; otherwise it opens one (skipping when main has nothing release
|
||||
# doesn't already have). Release CI (release-ci.yml) runs the full test +
|
||||
# E2E matrix on that PR; once it greens up a maintainer merges it and cuts a
|
||||
# release from `release` via release-staging.yml / release-production.yml.
|
||||
#
|
||||
# Two-job split on purpose: `check` runs on every merge to main with only the
|
||||
# default read token and no environment — the common case (PR already open)
|
||||
# stops there. `open-pr`, which claims the Release-PR-Automation environment
|
||||
# and mints a GitHub App token, only runs when a PR actually needs creating.
|
||||
name: Prepare Release PR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: prepare-release-pr
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check whether a main→release PR is needed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
needs-pr: ${{ steps.decide.outputs.needs_pr }}
|
||||
steps:
|
||||
- name: Decide (release branch exists, no open PR, main is ahead)
|
||||
id: decide
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
log() { echo "[ci][prepare-release-pr] $*"; }
|
||||
decide() { echo "needs_pr=$1" >> "$GITHUB_OUTPUT"; log "needs_pr=$1 ($2)"; }
|
||||
|
||||
if ! gh api "repos/${REPO}/branches/release" --silent 2>/dev/null; then
|
||||
decide false "release branch does not exist yet"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
existing="$(gh pr list --repo "${REPO}" --base release --head main --state open --json number --jq '.[0].number // empty')"
|
||||
if [ -n "${existing}" ]; then
|
||||
decide false "open main→release PR already exists: #${existing} (auto-updates as main advances)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip when main has no commits release lacks (e.g. right after a
|
||||
# release-back-merge) — GitHub rejects empty PRs anyway.
|
||||
ahead_by="$(gh api "repos/${REPO}/compare/release...main" --jq '.ahead_by')"
|
||||
log "main is ahead of release by ${ahead_by} commit(s)"
|
||||
if [ "${ahead_by}" = "0" ]; then
|
||||
decide false "release already contains main"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
decide true "no open PR and main is ahead by ${ahead_by}"
|
||||
|
||||
open-pr:
|
||||
name: Open the main→release PR
|
||||
needs: [check]
|
||||
if: needs.check.outputs.needs-pr == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
# The XGITHUB_APP_* secrets live in environments, never at repo level.
|
||||
# This dedicated environment holds ONLY those two secrets and is branch-
|
||||
# policied to `main`, so this job never sees the Production environment
|
||||
# (Apple signing, Tauri updater key, …) and no other ref can claim it.
|
||||
# It is only entered when `check` says a PR must be created.
|
||||
environment: Release-PR-Automation
|
||||
steps:
|
||||
# PRs created with the default GITHUB_TOKEN do not trigger pull_request
|
||||
# workflows (GitHub's recursion guard), which would leave the standing
|
||||
# main→release PR without a Release CI run until someone touched it.
|
||||
# Use the same GitHub App the release workflows push with.
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.XGITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
|
||||
# Scope the minted installation token to the minimum this job
|
||||
# needs: opening a PR. Unlike the release workflows' token, this
|
||||
# one can NOT push commits, so a compromise of this job cannot
|
||||
# write to protected branches. (This workflow only ever runs
|
||||
# trusted code — push:main / dispatch, never pull_request — this
|
||||
# is defense-in-depth.)
|
||||
permission-contents: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Open the main→release PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
log() { echo "[ci][prepare-release-pr] $*"; }
|
||||
|
||||
# Re-check under the concurrency group: a parallel run may have
|
||||
# opened the PR between `check` and this job.
|
||||
existing="$(gh pr list --repo "${REPO}" --base release --head main --state open --json number --jq '.[0].number // empty')"
|
||||
if [ -n "${existing}" ]; then
|
||||
log "open main→release PR appeared meanwhile: #${existing} — nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
url="$(gh pr create \
|
||||
--repo "${REPO}" \
|
||||
--base release \
|
||||
--head main \
|
||||
--title "Release: merge main into release" \
|
||||
--body "$(printf 'Auto-prepared by prepare-release-pr.yml on push to main.\n\nThis PR runs the full Release CI suite (unit + Rust E2E + Playwright + desktop E2E on 3 OSes). Maintainers can push fixes to it; merge once green to advance the release branch, then cut a release with release-staging.yml / release-production.yml.')")"
|
||||
log "opened ${url}"
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
# Promotes main into the long-lived `release` branch on demand.
|
||||
#
|
||||
# A maintainer dispatches this workflow to take a snapshot of main: it pushes
|
||||
# a merge commit from main into `release` directly (no PR). CI Full
|
||||
# (ci-full.yml) then runs the complete test + E2E matrix on that push. Fix
|
||||
# PRs for anything the full suite catches are opened directly against
|
||||
# `release` (they run ci-lite for quick feedback plus ci-full, whose
|
||||
# "CI Full Gate" check gates the merge). Releases are cut from `release` via
|
||||
# release-staging.yml / release-production.yml, both of which merge release
|
||||
# back into main.
|
||||
#
|
||||
# Merge, not reset: re-dispatching refreshes release with main's latest while
|
||||
# preserving fix commits already on release. When release already contains
|
||||
# main this is a no-op.
|
||||
name: Promote main to release
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: promote-main-to-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
name: Merge main into release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# The XGITHUB_APP_* secrets live in environments, never at repo level.
|
||||
# This dedicated environment holds ONLY those two secrets and is branch-
|
||||
# policied to `main`, so this job never sees the Production environment
|
||||
# (Apple signing, Tauri updater key, …) and no other ref can claim it.
|
||||
environment: Release-PR-Automation
|
||||
steps:
|
||||
# Pushes made with the default GITHUB_TOKEN do not trigger `push`
|
||||
# workflows (GitHub's recursion guard), which would leave the promotion
|
||||
# commit on `release` without a CI Full run. Use the same GitHub App
|
||||
# the release workflows push with; its identity is on the ruleset
|
||||
# bypass list for the protected branches.
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.XGITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
|
||||
# Least privilege: this job only pushes the promotion merge commit
|
||||
# to `release` — contents: write is all it needs.
|
||||
permission-contents: write
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Merge main into release and push
|
||||
env:
|
||||
APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
log() { echo "[ci][promote] $*"; }
|
||||
|
||||
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 main
|
||||
log "main is at $(git rev-parse origin/main)"
|
||||
|
||||
if ! git fetch origin release 2>/dev/null; then
|
||||
log "release branch does not exist — bootstrapping it from main"
|
||||
git push origin origin/main:refs/heads/release
|
||||
log "created release at $(git rev-parse origin/main)"
|
||||
exit 0
|
||||
fi
|
||||
log "release is at $(git rev-parse origin/release)"
|
||||
|
||||
if git merge-base --is-ancestor origin/main origin/release; then
|
||||
log "release already contains main — nothing to promote"
|
||||
echo "::notice::release already contains main; no promotion needed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git checkout -B release origin/release
|
||||
# Plain merge (fast-forward allowed): ff keeps history linear when
|
||||
# release carries no fix commits; when it does, git creates the
|
||||
# merge commit with this message.
|
||||
if ! git merge origin/main -m "chore(release): merge main into release"; then
|
||||
git merge --abort
|
||||
log "merge conflict between main and release"
|
||||
echo "::error::main→release merge hit conflicts. Resolve by opening a fix PR against release (or merge main into release manually), then re-dispatch."
|
||||
exit 1
|
||||
fi
|
||||
git push origin HEAD:release
|
||||
log "release advanced to $(git rev-parse HEAD)"
|
||||
@@ -24,11 +24,21 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
skip_ci_gate:
|
||||
description:
|
||||
Skip the green-CI-Full-Gate requirement on the commit being cut.
|
||||
Operator recovery only — never the default release path.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
# `actions: read` lets scripts/ci-cancel-aware.sh poll the run status so
|
||||
# cancelled builds inside container jobs stop themselves (docker exec
|
||||
# swallows the runner's signals).
|
||||
actions: read
|
||||
# `checks: read` lets scripts/release/require-ci-full-gate.sh verify the
|
||||
# "CI Full Gate" check run on the commit being cut.
|
||||
checks: read
|
||||
contents: write
|
||||
packages: write
|
||||
concurrency:
|
||||
@@ -166,6 +176,15 @@ jobs:
|
||||
git checkout release
|
||||
git pull origin release --ff-only
|
||||
fi
|
||||
# Direct App-token pushes bypass the PR merge gate, so nothing else
|
||||
# guarantees the commit being cut passed the full suite. Fail unless
|
||||
# the latest "CI Full Gate" check run on it (skipping [skip ci] bump
|
||||
# commits) concluded success.
|
||||
- name: Require green CI Full Gate
|
||||
if: ${{ !inputs.skip_ci_gate }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash scripts/release/require-ci-full-gate.sh "$(git rev-parse HEAD)"
|
||||
- name: Compute next version and sync release files
|
||||
id: bump
|
||||
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
|
||||
@@ -196,7 +215,9 @@ jobs:
|
||||
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}"
|
||||
# [skip ci]: the bump commit lands on an already-validated release
|
||||
# tree — don't re-trigger the full suite (ci-full.yml, push:release).
|
||||
git commit -m "chore(release): v${VERSION} [skip ci]"
|
||||
git push origin HEAD:release
|
||||
if [ "$CREATE_RELEASE" = "true" ]; then
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
@@ -237,24 +258,7 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "[release-production] merging release back into main"
|
||||
RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
git fetch origin main
|
||||
git checkout -B main origin/main
|
||||
# Fast-forward when main hasn't moved since the cut — this leaves
|
||||
# main == release, so prepare-release-pr sees ahead_by=0 and does
|
||||
# NOT open a no-op main→release PR for the back-merge commit.
|
||||
if git merge --ff-only "$RELEASE_SHA" 2>/dev/null; then
|
||||
git push origin HEAD:main
|
||||
echo "[release-production] fast-forwarded main to $RELEASE_SHA"
|
||||
elif git merge --no-ff "$RELEASE_SHA" -m "chore(release): merge release v${VERSION} back into main"; then
|
||||
git push origin HEAD:main
|
||||
echo "[release-production] merged $RELEASE_SHA into main (merge commit)"
|
||||
else
|
||||
echo "::warning::Automatic release→main back-merge hit conflicts. Merge branch 'release' into 'main' manually."
|
||||
exit 1
|
||||
fi
|
||||
bash scripts/release/merge-release-into-main.sh "chore(release): merge release v${VERSION} back into main"
|
||||
|
||||
# =========================================================================
|
||||
# Phase 2: Create draft GitHub release
|
||||
|
||||
@@ -20,11 +20,21 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
skip_ci_gate:
|
||||
description:
|
||||
Skip the green-CI-Full-Gate requirement on the commit being cut.
|
||||
Operator recovery only — never the default release path.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
# `actions: read` lets scripts/ci-cancel-aware.sh poll the run status so
|
||||
# cancelled builds inside container jobs stop themselves (docker exec
|
||||
# swallows the runner's signals).
|
||||
actions: read
|
||||
# `checks: read` lets scripts/release/require-ci-full-gate.sh verify the
|
||||
# "CI Full Gate" check run on the commit being cut.
|
||||
checks: read
|
||||
# `contents: write` is required for the patch bump commit and the
|
||||
# `v<version>-staging` tag push performed by `prepare-build` below.
|
||||
contents: write
|
||||
@@ -141,6 +151,15 @@ jobs:
|
||||
git checkout release
|
||||
git pull origin release --ff-only
|
||||
fi
|
||||
# Direct App-token pushes bypass the PR merge gate, so nothing else
|
||||
# guarantees the commit being cut passed the full suite. Fail unless
|
||||
# the latest "CI Full Gate" check run on it (skipping [skip ci] bump
|
||||
# commits) concluded success.
|
||||
- name: Require green CI Full Gate
|
||||
if: ${{ !inputs.skip_ci_gate }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash scripts/release/require-ci-full-gate.sh "$(git rev-parse HEAD)"
|
||||
# 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 `release` (we don't maintain a separate `staging`
|
||||
@@ -188,7 +207,9 @@ jobs:
|
||||
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}"
|
||||
# [skip ci]: the bump commit lands on an already-validated release
|
||||
# tree — don't re-trigger the full suite (ci-full.yml, push:release).
|
||||
git commit -m "chore(staging): v${VERSION} [skip ci]"
|
||||
git push origin HEAD:release
|
||||
if [ "$CREATE_TAG" = "true" ]; then
|
||||
git tag -a "$TAG" -m "Staging cut $TAG"
|
||||
@@ -221,6 +242,19 @@ jobs:
|
||||
fi
|
||||
echo "base_url=https://staging-api.tinyhumans.ai/" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Keep main in sync: the version-bump commit (and anything else on
|
||||
# release) flows back into main right after the cut — every release
|
||||
# build back-merges, not just production. Runs in both create_tag
|
||||
# variants because the bump commit lands on release in both.
|
||||
# continue-on-error so a merge conflict never strands a cut that is
|
||||
# already tagged — resolve the merge manually in that case.
|
||||
- name: Merge release back into main
|
||||
continue-on-error: true
|
||||
env:
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
run: |
|
||||
bash scripts/release/merge-release-into-main.sh "chore(staging): merge release v${VERSION}-staging back into main"
|
||||
|
||||
# =========================================================================
|
||||
# Phase 2: Build desktop artifacts (delegated to reusable workflow)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user