forked from tinyhumansai/openhuman
-
Notifications
You must be signed in to change notification settings - Fork 0
382 lines (377 loc) · 17.2 KB
/
Copy pathrelease-staging.yml
File metadata and controls
382 lines (377 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
---
name: Release Staging
on:
workflow_dispatch:
inputs:
skip_e2e:
description:
Skip the entire pretest phase (unit/rust plus E2E) and continue
directly to the desktop/docker staging build. Use only when the
required pretest signal is already known and you need to unblock a
staging cut.
required: false
type: boolean
default: false
permissions:
# `contents: write` is required for the patch bump commit and the
# `v<version>-staging` tag push performed by `prepare-build` below.
contents: write
packages: read
concurrency:
group: release-staging
cancel-in-progress: false
# ---------------------------------------------------------------------------
# Job dependency graph
#
# prepare-build
# │
# ├── pretest-tests (reusable test-reusable.yml — unit + rust;
# │ optional when `skip_e2e` is true)
# ├── pretest-e2e (reusable e2e-reusable.yml — all 3 OS, full suite;
# │ optional when `skip_e2e` is true)
# │
# ├── build-desktop (delegated to .github/workflows/build-desktop.yml)
# ├── build-docker (build only — no GHCR push on staging)
# │
# record-sentry-deploy
# │
# cleanup-failed-staging (on failure)
#
# The pretest jobs are a hard gate — `build-desktop` and `build-docker`
# only start once unit/rust/E2E have all passed across every target. This
# guarantees we never produce a staging tag whose installers were built
# against unproven code.
#
# The actual desktop build / Sentry / artifact-upload pipeline lives in
# `.github/workflows/build-desktop.yml` and is shared with
# release-production.yml.
# ---------------------------------------------------------------------------
jobs:
# =========================================================================
# Phase 1: Patch-bump on `main` and 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
runs-on: ubuntu-latest
# Reuse the Production GitHub Actions environment so Sentry vars
# (`OPENHUMAN_*_SENTRY_DSN`, `SENTRY_PROJECT_*`, `SENTRY_ORG`,
# `SENTRY_AUTH_TOKEN`) and `VITE_DEBUG` resolve here too. Staging
# events differentiate from production via the `environment` tag set
# at runtime — separate Sentry projects are not needed.
environment: Production
outputs:
version: ${{ steps.resolve.outputs.version }}
# Immutable staging tag created by this run, e.g. `v1.2.4-staging`.
# Downstream consumers (release-production.yml `staging_tag` promotion,
# Sentry, installer asset names) reference this rather than the bare SHA.
tag: ${{ steps.resolve.outputs.tag }}
sha: ${{ steps.resolve.outputs.sha }}
# First 12 chars of `sha`. Matches the truncation runtime code in
# config.ts / vite.config.ts / main.rs / app/src-tauri/src/lib.rs
# applies when computing `openhuman@<version>+<short_sha>`. Use this
# (not `sha`) anywhere CI constructs SENTRY_RELEASE so uploaded
# artifacts attach to the same release events report.
short_sha: ${{ steps.resolve.outputs.short_sha }}
build_ref: ${{ steps.resolve.outputs.build_ref }}
base_url: ${{ steps.resolve.outputs.base_url }}
steps:
- name: Enforce main branch
if: github.ref != 'refs/heads/main'
run: |
echo "This workflow can only run from main. Current ref: $GITHUB_REF"
exit 1
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.XGITHUB_APP_ID }}
private-key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
submodules: recursive
- name: Setup pnpm
uses: pnpm/action-setup@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 24.x
package-manager-cache: false
- name: Configure Git
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
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
# 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`
# branch) and the immutable `v<version>-staging` tag pinpoints the
# exact main commit QA validated, so production promotion can later
# find the tagged commit reachable from main.
- name: Bump patch version
id: bump
run: node scripts/release/bump-version.js patch
- name: Verify version sync
run: node scripts/release/verify-version-sync.js "${{ steps.bump.outputs.version }}"
- name: Refresh Cargo.lock files
# 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.
run: |
cargo update --workspace --manifest-path Cargo.toml
cargo update --workspace --manifest-path app/src-tauri/Cargo.toml
- name: Compute staging tag
id: tagname
env:
VERSION: ${{ steps.bump.outputs.version }}
run: |
STAGING_TAG="v${VERSION}-staging"
echo "tag=${STAGING_TAG}" >> "$GITHUB_OUTPUT"
- name: Ensure staging tag does not already exist
env:
TAG: ${{ steps.tagname.outputs.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: Commit, push and tag staging cut
id: push
env:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.tagname.outputs.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"
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Resolve build outputs
id: resolve
shell: bash
env:
VERSION: ${{ steps.bump.outputs.version }}
TAG: ${{ steps.tagname.outputs.tag }}
SHA: ${{ steps.push.outputs.sha }}
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"
echo "base_url=https://staging-api.tinyhumans.ai/" >> "$GITHUB_OUTPUT"
# =========================================================================
# Phase 1b: Pretest gate — run the full test + E2E suite across every
# target OS exactly 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_e2e }}
uses: ./.github/workflows/test-reusable.yml
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
pretest-e2e:
name: Pretest — E2E (all OS, full suite)
needs: [prepare-build]
if: ${{ !inputs.skip_e2e }}
uses: ./.github/workflows/e2e-reusable.yml
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
run_linux: true
run_macos: true
run_windows: true
full: true
# =========================================================================
# Phase 2: Build desktop artifacts (delegated to reusable workflow)
# =========================================================================
build-desktop:
name: Build desktop matrix
needs: [prepare-build, pretest-tests, pretest-e2e]
if: >-
always()
&& (needs.pretest-tests.result == 'success'
|| (inputs.skip_e2e && needs.pretest-tests.result == 'skipped'))
&& (needs.pretest-e2e.result == 'success'
|| (inputs.skip_e2e && needs.pretest-e2e.result == 'skipped'))
uses: ./.github/workflows/build-desktop.yml
secrets: inherit
with:
build_ref: ${{ needs.prepare-build.outputs.build_ref }}
tag: ${{ needs.prepare-build.outputs.tag }}
version: ${{ needs.prepare-build.outputs.version }}
sha: ${{ needs.prepare-build.outputs.sha }}
short_sha: ${{ needs.prepare-build.outputs.short_sha }}
base_url: ${{ needs.prepare-build.outputs.base_url }}
app_env: staging
build_profile: debug
telegram_bot_username: alphahumantest_bot
# Notarize staging too — QA installs the bundle from the Actions
# artifact, and unnotarized .app launches are blocked by Gatekeeper on
# macOS ≥ 10.15 (“damaged and can’t be opened”) without out-of-band
# `xattr -dr com.apple.quarantine` workarounds.
with_macos_signing: true
with_release_upload: false
# No publish-updater-manifest job in staging — producing .sig artifacts
# would just leave them stranded in the Actions artifact tree.
with_updater: false
# Standalone openhuman-core CLI ships from the production cut only:
# `build-docker` pushes `ghcr.io/tinyhumansai/openhuman-core` and
# `build-cli-linux` attaches Linux x86_64 / aarch64 tarballs to the
# GitHub Release (see release-production.yml). Staging does not
# publish either surface — the matrix-built sidecar artifact had no
# 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_e2e }}
# =========================================================================
# Phase 2b: Build the openhuman-core Docker image without pushing.
# Mirrors the production `build-docker` job so a Dockerfile regression
# surfaces on the staging cut — no GHCR push, no `:staging-*` tag
# pollution. `deploy-smoke.yml` already covers the build path on PRs
# that touch Dockerfile / src; this is the equivalent gate at the
# staging-tag boundary so a green staging cut means the next prod
# promotion's GHCR push will succeed too.
# =========================================================================
build-docker:
name: "Docker: build (no push)"
needs: [prepare-build, pretest-tests, pretest-e2e]
if: >-
always()
&& (needs.pretest-tests.result == 'success'
|| (inputs.skip_e2e && needs.pretest-tests.result == 'skipped'))
&& (needs.pretest-e2e.result == 'success'
|| (inputs.skip_e2e && needs.pretest-e2e.result == 'skipped'))
runs-on: ubuntu-latest
environment: Production
steps:
- name: Checkout build ref
uses: actions/checkout@v5
with:
ref: ${{ needs.prepare-build.outputs.build_ref }}
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: false
load: true
platforms: linux/amd64
tags: openhuman-core:staging-${{ needs.prepare-build.outputs.tag }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ needs.prepare-build.outputs.sha }}
org.opencontainers.image.version=${{ needs.prepare-build.outputs.version }}
org.opencontainers.image.title=openhuman-core
cache-from: type=gha,scope=release-staging
cache-to: type=gha,scope=release-staging,mode=max
# =========================================================================
# Phase 3: Record a single Sentry deploy marker once the matrix is
# complete. Lives in its own job (not inside the reusable workflow)
# because `sentry-cli releases deploys ... new` does NOT deduplicate by
# (release, env) — running it inside the matrix would add one row per
# platform (×4). One row per release is the right shape: re-runs of CI
# for the same release intentionally produce additional rows representing
# separate deploy attempts.
# =========================================================================
record-sentry-deploy:
name: Record Sentry deploy marker
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, build-desktop, build-docker]
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
steps:
- name: Install sentry-cli
if: env.SENTRY_AUTH_TOKEN != ''
shell: bash
run: curl -sSf https://sentry.io/get-cli/ | bash
- name: Record deploy marker
if: env.SENTRY_AUTH_TOKEN != ''
shell: bash
env:
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
# Marker lives on the React project's release; events from all
# surfaces share the same `openhuman@<version>+<short_sha>` release
# tag, so the marker on any single project's release shows in
# Sentry's "Deploys" tab for that release group.
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_REACT }}
SENTRY_RELEASE:
openhuman@${{ needs.prepare-build.outputs.version }}+${{
needs.prepare-build.outputs.short_sha }}
SENTRY_ENVIRONMENT: staging
run: |
set -euo pipefail
echo "==> Recording deploy marker: ${SENTRY_RELEASE} -> ${SENTRY_ENVIRONMENT}"
sentry-cli releases deploys "${SENTRY_RELEASE}" new \
-e "${SENTRY_ENVIRONMENT}"
# =========================================================================
# Cleanup: delete the staging tag if the build matrix failed. The version
# bump commit on `main` stays — reverting it would risk a race with
# concurrent merges. The next staging cut just continues from the new
# patch number; the small “gap” in patch numbers is acceptable.
# =========================================================================
cleanup-failed-staging:
name: Remove staging tag if build failed
runs-on: ubuntu-latest
environment: Production
needs: [prepare-build, pretest-tests, pretest-e2e, build-desktop, build-docker]
if: >-
always()
&& needs.prepare-build.result == 'success'
&& (needs.pretest-tests.result == 'failure' || needs.pretest-tests.result == 'cancelled'
|| needs.pretest-e2e.result == 'failure' || needs.pretest-e2e.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
uses: actions/github-script@v8
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const tag = '${{ needs.prepare-build.outputs.tag }}';
try {
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
core.info(`Deleted remote staging tag ${tag}`);
} catch (e) {
if (e.status === 404) {
core.info(`Staging tag ${tag} already absent on remote`);
} else {
throw e;
}
}