-
Notifications
You must be signed in to change notification settings - Fork 2.2k
375 lines (340 loc) · 17.5 KB
/
Copy pathdesktop_windows_release.yml
File metadata and controls
375 lines (340 loc) · 17.5 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
name: Auto Release Desktop (Windows) on Main
# Windows counterpart to desktop_auto_release.yml (macOS). Manual release only:
# workflow_dispatch -> bump the patch version, tag it, build the installer,
# publish it to a prerelease (beta) GitHub Release.
#
# Automatic push-to-main tagging was retired: every desktop/windows merge was
# minting a new v*-windows tag and opening a sync PR. Cut Windows releases
# deliberately via workflow_dispatch (release_now / force_release / next_version).
#
# Difference from macOS: macOS only tags and hands the build to Codemagic.
# Windows has no external CI, so this workflow ALSO builds + publishes, on a
# windows-latest runner, using electron-builder (NSIS) + gh.
#
# The git tag `v<version>-windows` is the source of truth for the version
# (mirrors macOS, where the tag — not a checked-in version field — drives
# releases). The version is stamped into package.json at build time from the
# tag; the same bump is synced back to main as a best-effort PR so the checked-in
# version tracks reality.
#
# electron-builder config: the build steps pass `--config electron-builder.config.mjs`
# explicitly. electron-builder only AUTO-detects electron-builder.<ext>; the
# `.config.mjs` name is NOT auto-detected, and that JS config is what computes the
# pi-mono asarUnpack closure at pack time. Dropping the flag would silently ship an
# installer missing that closure and break the coding agent. `pnpm build:win`
# already carries the flag; the signed path passes it by hand.
on:
workflow_dispatch:
inputs:
release_mode:
description: 'Release behavior'
required: false
default: 'release_now'
type: choice
options:
- release_now
- force_release
next_version:
description: 'Optional explicit version to tag (for example, 1.2.0)'
required: false
type: string
bypass_update_feed_probe_reason:
description: 'Emergency only: supply a tracking-issue URL or reason AND the literal phrase I ACKNOWLEDGE THE ROUTE IS REQUIRED to skip the probe. Empty or any other text = probe runs.'
required: false
type: string
permissions:
contents: write
pull-requests: write
concurrency:
# Serialize releases; never cancel a run mid-publish (a cancelled run can leave
# a pushed tag with no assets).
group: desktop-windows-release-main
cancel-in-progress: false
jobs:
plan-and-tag:
runs-on: ubuntu-latest
# Fast-path loop guard: never act on our own release-bump commit.
if: >-
github.event_name == 'workflow_dispatch' ||
!startsWith(github.event.head_commit.message, 'chore(windows): release v')
outputs:
should_release: ${{ steps.plan.outputs.should_release }}
version: ${{ steps.plan.outputs.version }}
release_tag: ${{ steps.plan.outputs.release_tag }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
# Windows clients since #10610 fail closed without this production route.
# Probe BOTH channels that real clients can request: beta (opt-in) and
# stable (the default for fresh installs and un-toggled settings). The
# backend never falls stable through to beta, so a beta-only probe passing
# while stable 404s would still ship a broken default cohort.
#
# Break-glass: the probe is skipped only when the dispatcher supplies the
# literal confirmation sentinel "I ACKNOWLEDGE THE ROUTE IS REQUIRED" plus a
# tracking-issue/reason. An empty or arbitrary value still runs the probe.
# The sentinel is checked through an env var (not direct interpolation) so
# operator-supplied reason text is treated as data, not shell syntax.
- name: Probe production Windows update-feed (beta + stable)
if: ${{ !contains(github.event.inputs.bypass_update_feed_probe_reason, 'I ACKNOWLEDGE THE ROUTE IS REQUIRED') }}
shell: bash
run: |
python3 .github/scripts/probe_windows_update_feed.py \
--channel beta \
--channel stable
- name: Probe bypassed (audited emergency release)
if: ${{ contains(github.event.inputs.bypass_update_feed_probe_reason, 'I ACKNOWLEDGE THE ROUTE IS REQUIRED') }}
shell: bash
env:
BYPASS_REASON: ${{ github.event.inputs.bypass_update_feed_probe_reason }}
run: |
echo "::warning::Windows update-feed probe BYPASSED."
printf 'Reason: %s\n' "$BYPASS_REASON"
echo "This is recorded for audit. The route deploy (#10610) is still required."
- name: Plan release (compute next version, tag, sync back to main)
id: plan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_MODE: ${{ github.event.inputs.release_mode || 'release_now' }}
NEXT_VERSION: ${{ github.event.inputs.next_version || '' }}
shell: bash
run: |
set -euo pipefail
set_output() { echo "$1=$2" >> "$GITHUB_OUTPUT"; }
# --- Latest Windows release tag (source of truth for the version) ---
LATEST=$(git tag -l 'v*-windows' | sort -V | tail -1)
echo "Latest windows tag: ${LATEST:-none}"
# --- Is there a releasable desktop/windows change since that tag? ---
# (git diff --quiet exits 1 on differences; keep it out of a `head`
# pipe so pipefail+SIGPIPE can't abort the step.)
HAS_CHANGES=false
if [ -z "$LATEST" ]; then
[ -n "$(git ls-files desktop/windows)" ] && HAS_CHANGES=true
elif ! git diff --quiet --diff-filter=ACDMR "${LATEST}..HEAD" -- desktop/windows; then
HAS_CHANGES=true
fi
if [ "$HAS_CHANGES" != "true" ] && [ "$RELEASE_MODE" != "force_release" ]; then
echo "No releasable desktop/windows changes since ${LATEST:-repo start}."
set_output should_release false
exit 0
fi
# --- Compute the next version (patch bump) ---
if [ -n "$NEXT_VERSION" ]; then
if ! [[ "$NEXT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "next_version must be a semantic version like 1.2.0" >&2
exit 1
fi
VERSION="$NEXT_VERSION"
else
if [ -n "$LATEST" ]; then
BASE=$(echo "$LATEST" | sed -E 's/^v(.+)-windows$/\1/')
else
# First release: continue from the checked-in version.
BASE=$(node -p "require('./desktop/windows/package.json').version")
fi
MAJOR=$(echo "$BASE" | cut -d. -f1)
MINOR=$(echo "$BASE" | cut -d. -f2)
PATCH=$(echo "$BASE" | cut -d. -f3)
PATCH=$(( ${PATCH:-0} + 1 ))
VERSION="${MAJOR}.${MINOR}.${PATCH}"
fi
RELEASE_TAG="v${VERSION}-windows"
echo "New version: $VERSION"
echo "New tag : $RELEASE_TAG"
if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then
echo "Tag ${RELEASE_TAG} already exists — aborting to avoid clobbering a release." >&2
exit 1
fi
# --- Stamp package.json and tag that commit ---
# The commit lives on the tag only; main receives it via the sync PR
# below. Pushing the tag carries the bump commit to origin, so the build
# job checks out the tag and gets the right version.
node -e "const f='desktop/windows/package.json',fs=require('fs');const j=JSON.parse(fs.readFileSync(f));j.version='${VERSION}';fs.writeFileSync(f,JSON.stringify(j,null,2)+'\n');"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add desktop/windows/package.json
git commit -m "chore(windows): release v${VERSION}"
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
set_output should_release true
set_output version "$VERSION"
set_output release_tag "$RELEASE_TAG"
# --- Best-effort: sync the version bump back to main via PR ---
# main may be protected; if the PR cannot be merged automatically the
# release still succeeded (the tag is authoritative) and the PR waits
# for a manual merge. GITHUB_TOKEN pushes do not re-trigger this workflow.
BRANCH="release/windows-v${VERSION}"
git checkout -B "$BRANCH"
git push --force-with-lease origin "$BRANCH" || {
echo "Could not push sync branch; skipping main sync (release already tagged).";
exit 0;
}
PR_NUMBER=$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[0].number' || echo "")
if [ -z "$PR_NUMBER" ]; then
gh pr create \
--title "chore(windows): sync release v${VERSION} to main [skip ci]" \
--body "Auto-generated: stamps desktop/windows/package.json to v${VERSION} to match the ${RELEASE_TAG} release." \
--base main --head "$BRANCH" || {
echo "Could not open sync PR (non-fatal).";
exit 0;
}
PR_NUMBER=$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[0].number' || echo "")
fi
if [ -n "$PR_NUMBER" ]; then
gh pr merge "$PR_NUMBER" --merge --admin || \
gh pr merge "$PR_NUMBER" --merge --auto || \
gh pr merge "$PR_NUMBER" --merge || \
echo "Sync PR #$PR_NUMBER needs a manual merge (release already published)."
fi
build-and-publish:
needs: [plan-and-tag]
if: needs.plan-and-tag.outputs.should_release == 'true'
runs-on: windows-latest
defaults:
run:
working-directory: desktop/windows
steps:
- name: Checkout release tag
uses: actions/checkout@v7
with:
ref: ${{ needs.plan-and-tag.outputs.release_tag }}
# Re-probe at publish time: if build-and-publish is retried (re-run failed
# jobs), GitHub Actions reuses the cached plan-and-tag outputs and does not
# re-execute the probe that ran there. Re-validate the route now so a
# backend rollback between tagging and publishing cannot mint a broken cohort.
- name: Setup Python for re-probe
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Re-probe production Windows update-feed before publish
if: ${{ !contains(github.event.inputs.bypass_update_feed_probe_reason, 'I ACKNOWLEDGE THE ROUTE IS REQUIRED') }}
shell: bash
working-directory: ${{ github.workspace }}
run: |
python .github/scripts/probe_windows_update_feed.py \
--channel beta \
--channel stable
- uses: pnpm/action-setup@v6
with:
version: 10
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
cache-dependency-path: desktop/windows/pnpm-lock.yaml
# OCR + UI-automation helpers are .NET projects built during install/build.
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Provision .env (ships public Firebase/PostHog config)
shell: pwsh
run: Copy-Item .env.example .env
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Sign only when Azure Trusted Signing secrets are present. When absent the
# installer is built unsigned (Windows SmartScreen shows "unknown
# publisher"); the release notes say so. See docs/release-pipeline.md.
- name: Detect signing secrets
id: signing
shell: bash
env:
# All-or-nothing: every secret the signed build path needs (auth +
# profile) must be present, or a partial set would run the signed path
# with an empty value and die at signing instead of falling back unsigned.
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_PUBLISHER_NAME: ${{ secrets.AZURE_PUBLISHER_NAME }}
AZURE_ENDPOINT: ${{ secrets.AZURE_CODE_SIGNING_ENDPOINT }}
AZURE_ACCOUNT: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT }}
AZURE_PROFILE: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
run: |
if [ -n "${AZURE_TENANT_ID}" ] && [ -n "${AZURE_CLIENT_ID}" ] && [ -n "${AZURE_CLIENT_SECRET}" ] && \
[ -n "${AZURE_PUBLISHER_NAME}" ] && [ -n "${AZURE_ENDPOINT}" ] && [ -n "${AZURE_ACCOUNT}" ] && [ -n "${AZURE_PROFILE}" ]; then
echo "signed=true" >> "$GITHUB_OUTPUT"
else
echo "signed=false" >> "$GITHUB_OUTPUT"
fi
- name: Build signed installer (Azure Trusted Signing)
if: steps.signing.outputs.signed == 'true'
shell: bash
env:
# Auth (electron-builder / @azure/identity read these from the env).
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
# Signing profile (injected as electron-builder config overrides so the
# committed config stays inert for unsigned local builds).
AZURE_PUBLISHER_NAME: ${{ secrets.AZURE_PUBLISHER_NAME }}
AZURE_ENDPOINT: ${{ secrets.AZURE_CODE_SIGNING_ENDPOINT }}
AZURE_ACCOUNT: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT }}
AZURE_PROFILE: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
# --config electron-builder.config.mjs is REQUIRED — that JS config computes
# the pi-mono asarUnpack closure; without it electron-builder auto-detects no
# config and ships an installer that breaks the coding agent.
run: |
npm run build
pnpm exec electron-builder --win --x64 --config electron-builder.config.mjs --publish never \
-c.win.azureSignOptions.publisherName="$AZURE_PUBLISHER_NAME" \
-c.win.azureSignOptions.endpoint="$AZURE_ENDPOINT" \
-c.win.azureSignOptions.certificateProfileName="$AZURE_PROFILE" \
-c.win.azureSignOptions.codeSigningAccountName="$AZURE_ACCOUNT"
- name: Build unsigned installer
if: steps.signing.outputs.signed != 'true'
shell: bash
# pnpm build:win == `npm run build && electron-builder --win --x64 --config
# electron-builder.config.mjs --publish never` — it carries the required config
# flag AND --publish never. The latter matters: the config's `publish` block is
# the electron-updater FEED pointer, but electron-builder also treats it as an
# upload target and auto-publishes when CI + a git tag are detected (this job
# checks out the release tag). Without --publish never it dies on a missing
# GH_TOKEN; the real upload happens in the explicit `gh release` step below.
run: pnpm build:win
- name: Collect release artifacts
shell: bash
run: |
set -euo pipefail
# electron-builder (NSIS) writes the installer, its blockmap, and the
# electron-updater feed metadata (latest.yml) to dist/.
ls -la dist || true
test -f dist/latest.yml || { echo "latest.yml missing — auto-update feed would be broken." >&2; exit 1; }
compgen -G "dist/*.exe" >/dev/null || { echo "installer .exe missing." >&2; exit 1; }
compgen -G "dist/*.exe.blockmap" >/dev/null || { echo "installer blockmap missing." >&2; exit 1; }
# Canonical stable-name copy the backend download endpoints resolve
# (backend/routers/updates.py matches exactly `omi-setup.exe`, the
# case-sensitive Windows analog of macOS's `omi.dmg`). The dist/*.exe
# glob in the upload step below picks it up automatically.
installer=$(compgen -G "dist/Omi-for-Windows-Setup-*.exe" | head -1)
test -n "$installer" || { echo "versioned installer not found for canonical copy." >&2; exit 1; }
cp "$installer" dist/omi-setup.exe
- name: Publish prerelease (beta) GitHub Release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.plan-and-tag.outputs.release_tag }}
VERSION: ${{ needs.plan-and-tag.outputs.version }}
SIGNED: ${{ steps.signing.outputs.signed }}
run: |
set -euo pipefail
if [ "$SIGNED" = "true" ]; then
SIGN_NOTE="Signed with Azure Trusted Signing."
else
SIGN_NOTE="**Unsigned build** — Windows SmartScreen will warn \"unknown publisher\". Signing activates automatically once the Azure Trusted Signing secrets are set (see desktop/windows/docs/release-pipeline.md)."
fi
NOTES="Automated Windows beta build for v${VERSION}.
${SIGN_NOTE}
Install: download and run the \`.exe\`. Installed apps auto-update through \`/v2/desktop/update-feed/windows\` (beta when Settings → Receive beta updates is on; otherwise stable)."
# Create the release if the plan job did not (idempotent on re-run).
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release create "$RELEASE_TAG" \
--title "Omi for Windows ${VERSION} (beta)" \
--notes "$NOTES" \
--prerelease
fi
# --clobber so a re-run overwrites partial uploads.
gh release upload "$RELEASE_TAG" --clobber \
dist/*.exe \
dist/*.exe.blockmap \
dist/latest.yml