-
Notifications
You must be signed in to change notification settings - Fork 14
745 lines (683 loc) · 33.8 KB
/
Copy pathrelease.yml
File metadata and controls
745 lines (683 loc) · 33.8 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
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
name: Release Build
# 签名/公证/上传全部依赖 CI secrets(单机无法完成)。secret/vars 名照搬 oo-desktop。
# 韧性步骤(OIDC 重试、rclone 缓存、Windows pnpm install 重试与自动更新产物校验、release-size
# 体积统计、富 Release notes、CDN 缓存刷新)与 oo-desktop 对齐;仅按 Wanta 适配:
# 标准 GitHub runner(非 blacksmith 池)、产物名 Wanta-*、OSS 前缀 release/apps/wanta、
# 安装期注入 OO_SKIP_BINARY_DOWNLOAD(oo 二进制由 build:* 内的 prepare:binaries 构建时下载)。
#
# 双渠道(Wanta 相对 oo-desktop 的 deliberate divergence,见 docs/key-decisions.md §10):
# stable 发 X.Y.Z(latest*.yml,gh release --latest),beta 发 X.Y.Z-beta.N(beta*.yml,
# --prerelease)。同一 OSS 目录下两套渠道指针并存;electron-builder 的
# generateUpdatesFilesForAllChannels 让 stable 构建同步刷新 beta*.yml(beta 用户立即收敛)。
# 版本计算在 scripts/release-version.ts(含 stable bump 过滤 beta tag 的关键防护)。
on:
workflow_dispatch:
inputs:
channel:
description: "发布渠道"
required: true
type: choice
options: [stable, beta]
default: stable
expected_version:
description: "预期版本号 (stable: 1.2.3 / beta: 1.2.3-beta.1, 留空则自动计算)"
required: false
type: string
default: ""
version_bump:
description: "自增版本位置 (仅 stable 且未指定预期版本号时生效)"
required: true
type: choice
options: [patch, minor, major]
default: patch
permissions:
contents: write
id-token: write
# 串行化发布:并发 dispatch 会算出同一版本号并竞写同一批 OSS 对象/渠道指针。
concurrency:
group: release
cancel-in-progress: false
env:
NODE_OPTIONS: "--max-old-space-size=8192"
OSS_BUCKET: oomol-static-cn-prod
OSS_PREFIX: release/apps/wanta
jobs:
compute-version:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.version.outputs.VERSION }}
channel: ${{ steps.version.outputs.CHANNEL }}
refresh_beta: ${{ steps.version.outputs.REFRESH_BETA }}
cdn_files: ${{ steps.version.outputs.CDN_FILES }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-node@v7
with:
node-version: "22"
# 本 job 只跑 node scripts/release-version.ts,从不安装依赖。
# setup-node@v5 默认 package-manager-cache: true 会自动探测
# pnpm-lock.yaml 并调用 pnpm 算缓存 key,但此处未装 pnpm,直接
# 报 "Unable to locate executable file: pnpm"。setup-node@v6 起自动
# 缓存只认 npm、不再探测 pnpm,这里保留显式关闭作为防御(注意
# cache: "" 关不掉这个自动探测,必须用 package-manager-cache)。
package-manager-cache: false
- name: Compute version
id: version
# 用户输入经 env 传递(不直接插值进脚本体,防 shell 注入/引号破碎)。
env:
INPUT_CHANNEL: ${{ inputs.channel }}
INPUT_EXPECTED: ${{ inputs.expected_version }}
INPUT_BUMP: ${{ inputs.version_bump }}
run: |
set -euo pipefail
CHANNEL="$INPUT_CHANNEL"
# 版本计算在 scripts/release-version.ts(纯函数 + vitest 覆盖):stable 自动 bump
# 过滤全部 beta tag(bash 算术遇 v1.0.1-beta.2 会爆炸的地雷在脚本里有回归用例);
# beta 基线 = max(最新 stable 的 patch+1, 既存 beta 最高基线),N 按基线递增;
# 显式版本有防回退校验。输出 JSON:{"version":..., "refreshBeta":...}。
RESULT=$(git tag -l 'v*' | node --experimental-strip-types scripts/release-version.ts \
--channel "$CHANNEL" \
--expected "$INPUT_EXPECTED" \
--bump "$INPUT_BUMP")
VERSION=$(echo "$RESULT" | jq -r '.version')
REFRESH_BETA=$(echo "$RESULT" | jq -r '.refreshBeta')
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "::error::Tag v$VERSION already exists"; exit 1
fi
# CDN 待刷新的渠道指针清单。stable 构建经 generateUpdatesFilesForAllChannels
# 同时刷新 beta*.yml(beta 用户立即收敛到正式版);唯一例外 REFRESH_BETA=false——
# 本次 stable 低于既存 beta 基线(手动抬过基线后的 hotfix),跳过 beta 指针防倒退。
{
echo "CDN_FILES<<CDN_EOF"
if [ "$CHANNEL" = "stable" ]; then
echo "/release/apps/wanta/darwin/arm64/latest-mac.yml"
echo "/release/apps/wanta/win32/x64/latest.yml"
fi
if [ "$REFRESH_BETA" = "true" ]; then
echo "/release/apps/wanta/darwin/arm64/beta-mac.yml"
echo "/release/apps/wanta/win32/x64/beta.yml"
fi
echo "CDN_EOF"
} >> "$GITHUB_OUTPUT"
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "CHANNEL=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "REFRESH_BETA=$REFRESH_BETA" >> "$GITHUB_OUTPUT"
echo "::notice::Release version: $VERSION (channel: $CHANNEL, refresh beta pointer: $REFRESH_BETA)"
release-mac:
needs: compute-version
runs-on: macos-latest
timeout-minutes: 60
env:
PINNED_RCLONE_VERSION: v1.69.1
OSS_ENDPOINT: https://oss-accelerate.aliyuncs.com
OSS_REGION: cn-hangzhou
CHANNEL: ${{ needs.compute-version.outputs.channel }}
REFRESH_BETA: ${{ needs.compute-version.outputs.refresh_beta }}
steps:
- uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Set env + version
run: |
echo "VERSION=${{ needs.compute-version.outputs.version }}" >> $GITHUB_ENV
corepack pnpm version "${{ needs.compute-version.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Import macOS signing certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
KEYCHAIN_PASSWORD=$(openssl rand -base64 24)
echo -n "$MACOS_CERTIFICATE" | base64 --decode -o "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PWD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | xargs)
rm -f "$CERT_PATH"
- name: Install dependencies
run: corepack pnpm install --frozen-lockfile
env:
npm_config_audit: "false"
npm_config_fund: "false"
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# oo 二进制由后续 build:* 里的 prepare:binaries 在构建时下载,无需 postinstall 重复拉取。
OO_SKIP_BINARY_DOWNLOAD: "1"
- name: Build (sign + notarize)
run: corepack pnpm run build:mac
env:
CSC_IDENTITY_AUTO_DISCOVERY: "true"
APPLE_ID: ${{ secrets.APPLEID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEID_PASS }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# 对齐 win 侧的产物硬校验:mac 自动更新器消费 <channel>-mac.yml + zip + zip.blockmap。
# stable 构建经 generateUpdatesFilesForAllChannels 同时产出 beta-mac.yml,两个指针都校验;
# 任一缺失/指错版本都让 release 在上传前大声失败。
- name: Verify macOS auto-update artifacts
run: |
set -euo pipefail
# 枚举守卫:outputs plumbing 回归(拼错输出名→空串)必须大声失败——
# 空串会让 stable 静默漏传 beta 指针、让 beta 在 bash≥4.4 上空 include 上传。
case "$CHANNEL" in stable|beta) ;; *) echo "::error::CHANNEL must be stable|beta, got '$CHANNEL'"; exit 1;; esac
case "$REFRESH_BETA" in true|false) ;; *) echo "::error::REFRESH_BETA must be true|false, got '$REFRESH_BETA'"; exit 1;; esac
RELEASE_DIR="release/${VERSION}"
YMLS=()
if [ "$CHANNEL" = "stable" ]; then
YMLS+=("latest-mac.yml")
fi
if [ "$REFRESH_BETA" = "true" ]; then
YMLS+=("beta-mac.yml")
fi
for NAME in "${YMLS[@]}"; do
YML="${RELEASE_DIR}/${NAME}"
if [ ! -f "$YML" ]; then
echo "::error::${NAME} was not produced at $YML"
ls -la "$RELEASE_DIR" || true
exit 1
fi
# `grep -Fq`(定长匹配)使 $VERSION 里的点被字面匹配。
if ! grep -Fq "Wanta-${VERSION}.zip" "$YML"; then
echo "::error::${NAME} does not reference Wanta-${VERSION}.zip"
cat "$YML"
exit 1
fi
done
BLOCKMAP="${RELEASE_DIR}/Wanta-${VERSION}.zip.blockmap"
if [ ! -f "$BLOCKMAP" ]; then
echo "::error::Differential-update blockmap missing: $BLOCKMAP"
ls -la "$RELEASE_DIR" || true
exit 1
fi
# 从本地构建产物采集 installer + app-bundle 体积,写成小 JSON 元数据,供
# create-release job 渲染进 Release notes,无需重新下载数百 MiB 的安装包。
- name: Collect macOS release size metadata
run: |
node --experimental-strip-types scripts/release-size.ts collect \
--platform darwin \
--arch arm64 \
--version "$VERSION" \
--release-dir "release/$VERSION" \
--out "release/$VERSION/release-size-darwin-arm64.json"
- name: Upload macOS release size metadata
uses: actions/upload-artifact@v7
with:
name: release-size-darwin-arm64
path: release/${{ env.VERSION }}/release-size-darwin-arm64.json
if-no-files-found: error
retention-days: 7
# OIDC ID token 获取偶发在 VM runner 上以 "Client network socket disconnected
# before secure TLS connection was established" 失败。单次重试即可吸收瞬时抖动;
# 两次都失败时 job 仍会抛出第二次的错误并照常失败。
- name: Configure Alibaba Cloud CLI
id: aliyun_credentials
uses: aliyun/configure-aliyun-credentials-action@v1.1.0
with:
role-to-assume: ${{ vars.ALIYUN_OOMOL_WANTA_ROLE }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER }}
role-session-name: github-action-session
role-session-expiration: 1800
audience: https://github.com/oomol-lab
continue-on-error: true
- name: Configure Alibaba Cloud CLI (retry)
id: aliyun_credentials_retry
if: steps.aliyun_credentials.outcome == 'failure'
uses: aliyun/configure-aliyun-credentials-action@v1.1.0
with:
role-to-assume: ${{ vars.ALIYUN_OOMOL_WANTA_ROLE }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER }}
role-session-name: github-action-session
role-session-expiration: 1800
audience: https://github.com/oomol-lab
- name: Cache rclone
uses: actions/cache@v6
with:
path: ~/.local/bin/rclone
key: rclone-${{ runner.os }}-${{ runner.arch }}-${{ env.PINNED_RCLONE_VERSION }}
- name: Setup rclone
run: |
set -euo pipefail
if [ ! -x "$HOME/.local/bin/rclone" ]; then
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://downloads.rclone.org/${PINNED_RCLONE_VERSION}/rclone-${PINNED_RCLONE_VERSION}-osx-arm64.zip" -o /tmp/rclone.zip
unzip -j /tmp/rclone.zip "rclone-${PINNED_RCLONE_VERSION}-osx-arm64/rclone" -d "$HOME/.local/bin"
chmod +x "$HOME/.local/bin/rclone"
fi
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Configure rclone for OSS
env:
# 读取重试成功的那次输出 —— 被跳过/失败步骤的 output 是空串,GitHub Actions
# 表达式 `||` 会在空串上回退到另一侧。
ALICLOUD_ACCESS_KEY_ID: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-id || steps.aliyun_credentials_retry.outputs.aliyun-access-key-id }}
ALICLOUD_ACCESS_KEY_SECRET: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-secret || steps.aliyun_credentials_retry.outputs.aliyun-access-key-secret }}
ALICLOUD_SECURITY_TOKEN: ${{ steps.aliyun_credentials.outputs.aliyun-security-token || steps.aliyun_credentials_retry.outputs.aliyun-security-token }}
run: |
set -euo pipefail
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf <<EOF
[oss]
type = s3
provider = Alibaba
access_key_id = ${ALICLOUD_ACCESS_KEY_ID}
secret_access_key = ${ALICLOUD_ACCESS_KEY_SECRET}
endpoint = ${OSS_ENDPOINT}
region = ${OSS_REGION}
session_token = ${ALICLOUD_SECURITY_TOKEN}
EOF
- name: Upload macOS artifacts to OSS
run: |
set -euo pipefail
# 渠道指针按渠道收紧 include 白名单:beta 发布绝不触碰 latest-mac.yml——
# 这份白名单就是 stable 指针的保护栏;stable 同步刷新 beta-mac.yml(见 builder 配置),
# 除非 REFRESH_BETA=false(本次 stable 低于既存 beta 基线,跳过防指针倒退)。
YML_INCLUDES=()
if [ "$CHANNEL" = "stable" ]; then
YML_INCLUDES+=(--include "latest-mac.yml")
fi
if [ "$REFRESH_BETA" = "true" ]; then
YML_INCLUDES+=(--include "beta-mac.yml")
fi
rclone copy "release/${VERSION}" "oss:${OSS_BUCKET}/${OSS_PREFIX}/darwin/arm64/" \
--include "Wanta-${VERSION}.dmg" --include "Wanta-${VERSION}.dmg.blockmap" \
--include "Wanta-${VERSION}.zip" --include "Wanta-${VERSION}.zip.blockmap" \
"${YML_INCLUDES[@]}" --progress --retries 5 --retries-sleep 10s --low-level-retries 10
release-win:
needs: compute-version
runs-on: [self-hosted, Windows, x64]
timeout-minutes: 60
env:
PINNED_RCLONE_VERSION: v1.69.1
OSS_ENDPOINT: https://oss-cn-hangzhou.aliyuncs.com
OSS_REGION: cn-hangzhou
CHANNEL: ${{ needs.compute-version.outputs.channel }}
REFRESH_BETA: ${{ needs.compute-version.outputs.refresh_beta }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
# This self-hosted runner cannot authenticate to the GitHub Actions
# cache service, so setup-node@v5's default package-manager cache
# (package-manager-cache: true) hangs ~10 min on a failed restore
# ("Server failed to authenticate the request") before giving up —
# setup-node took 20 min while the actual `pnpm install` took 34 s (run
# 29923188224). The runner's disk already persists pnpm's store
# across runs, so the GitHub-side cache is pure overhead here.
# setup-node@v6+ only auto-caches for npm, but keep this explicit
# off as a guard. Hosted runners (release-mac, pr.yml) keep their
# explicit `cache: pnpm`.
package-manager-cache: false
- name: Set env + version
shell: bash
run: |
echo "VERSION=${{ needs.compute-version.outputs.version }}" >> $GITHUB_ENV
corepack pnpm version "${{ needs.compute-version.outputs.version }}" --no-git-tag-version --allow-same-version
# 自托管 Windows runner 上 pnpm install 偶发网络/锁文件抖动;重试 3 次、每次重试前
# 清掉 node_modules,避免半装状态污染下一次安装。
- name: Install dependencies
uses: nick-fields/retry@v4
with:
max_attempts: 3
timeout_minutes: 15
shell: bash
command: corepack pnpm install --frozen-lockfile
on_retry_command: rm -rf node_modules
env:
npm_config_audit: "false"
npm_config_fund: "false"
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# oo 二进制由后续 build:* 里的 prepare:binaries 在构建时下载,无需 postinstall 重复拉取。
OO_SKIP_BINARY_DOWNLOAD: "1"
- name: Build (signtool via electron-builder certificateSha1)
run: corepack pnpm run build:win
# 硬防 electron-builder 未来行为漂移。NSIS 自动更新器消费 <channel>.yml + Setup.exe
# + Setup.exe.blockmap;任一不变量回归即让 release 大声失败。
# stable 构建经 generateUpdatesFilesForAllChannels 同时产出 beta.yml,两个指针都校验。
- name: Verify Windows auto-update artifacts
shell: bash
run: |
set -euo pipefail
# 枚举守卫:见 mac 侧同名步骤说明。
case "$CHANNEL" in stable|beta) ;; *) echo "::error::CHANNEL must be stable|beta, got '$CHANNEL'"; exit 1;; esac
case "$REFRESH_BETA" in true|false) ;; *) echo "::error::REFRESH_BETA must be true|false, got '$REFRESH_BETA'"; exit 1;; esac
RELEASE_DIR="release/${VERSION}"
YMLS=()
if [ "$CHANNEL" = "stable" ]; then
YMLS+=("latest.yml")
fi
if [ "$REFRESH_BETA" = "true" ]; then
YMLS+=("beta.yml")
fi
for NAME in "${YMLS[@]}"; do
YML="${RELEASE_DIR}/${NAME}"
if [ ! -f "$YML" ]; then
echo "::error::${NAME} was not produced at $YML"
ls -la "$RELEASE_DIR" || true
exit 1
fi
# `grep -Fq`(定长匹配)使 $VERSION 里的点被字面匹配——正则 `.` 会接受
# 像 Wanta-1x2x3-Setup.exe 这类病态文件名。
if ! grep -Fq "Wanta-${VERSION}-Setup.exe" "$YML"; then
echo "::error::${NAME} does not reference Wanta-${VERSION}-Setup.exe"
cat "$YML"
exit 1
fi
# Wanta 的 win target 只有 nsis(无 zip)。NSIS 自动更新器只消费 yml +
# Setup.exe + Setup.exe.blockmap,从不读 Setup.zip。若日后误给 electron-builder
# 加回 win zip target、让 yml 引到 Setup.zip,这里大声失败拦住回归。
if grep -Fq "Wanta-${VERSION}-Setup.zip" "$YML"; then
echo "::error::${NAME} unexpectedly references Setup.zip (Windows builds nsis only)"
cat "$YML"
exit 1
fi
done
BLOCKMAP="${RELEASE_DIR}/Wanta-${VERSION}-Setup.exe.blockmap"
if [ ! -f "$BLOCKMAP" ]; then
echo "::error::Differential-update blockmap missing: $BLOCKMAP"
ls -la "$RELEASE_DIR" || true
exit 1
fi
- name: Collect Windows release size metadata
shell: bash
run: |
node --experimental-strip-types scripts/release-size.ts collect \
--platform win32 \
--arch x64 \
--version "$VERSION" \
--release-dir "release/$VERSION" \
--out "release/$VERSION/release-size-win32-x64.json"
- name: Upload Windows release size metadata
uses: actions/upload-artifact@v7
with:
name: release-size-win32-x64
path: release/${{ env.VERSION }}/release-size-win32-x64.json
if-no-files-found: error
retention-days: 7
# 见 release-mac 处同名步骤的说明;自托管 Windows runner 同样观测到该 OIDC TLS 抖动。
- name: Configure Alibaba Cloud CLI
id: aliyun_credentials
uses: aliyun/configure-aliyun-credentials-action@v1.1.0
with:
role-to-assume: ${{ vars.ALIYUN_OOMOL_WANTA_ROLE }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER }}
role-session-name: github-action-session
role-session-expiration: 1800
audience: https://github.com/oomol-lab
continue-on-error: true
- name: Configure Alibaba Cloud CLI (retry)
id: aliyun_credentials_retry
if: steps.aliyun_credentials.outcome == 'failure'
uses: aliyun/configure-aliyun-credentials-action@v1.1.0
with:
role-to-assume: ${{ vars.ALIYUN_OOMOL_WANTA_ROLE }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER }}
role-session-name: github-action-session
role-session-expiration: 1800
audience: https://github.com/oomol-lab
- name: Setup rclone
shell: bash
run: |
set -euo pipefail
if [ ! -x "$HOME/.local/bin/rclone.exe" ]; then
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://downloads.rclone.org/${PINNED_RCLONE_VERSION}/rclone-${PINNED_RCLONE_VERSION}-windows-amd64.zip" -o /tmp/rclone.zip
unzip -j /tmp/rclone.zip "rclone-${PINNED_RCLONE_VERSION}-windows-amd64/rclone.exe" -d "$HOME/.local/bin"
fi
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Configure rclone for OSS
shell: bash
env:
ALICLOUD_ACCESS_KEY_ID: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-id || steps.aliyun_credentials_retry.outputs.aliyun-access-key-id }}
ALICLOUD_ACCESS_KEY_SECRET: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-secret || steps.aliyun_credentials_retry.outputs.aliyun-access-key-secret }}
ALICLOUD_SECURITY_TOKEN: ${{ steps.aliyun_credentials.outputs.aliyun-security-token || steps.aliyun_credentials_retry.outputs.aliyun-security-token }}
run: |
set -euo pipefail
mkdir -p "$HOME/.config/rclone"
cat > "$HOME/.config/rclone/rclone.conf" <<EOF
[oss]
type = s3
provider = Alibaba
access_key_id = ${ALICLOUD_ACCESS_KEY_ID}
secret_access_key = ${ALICLOUD_ACCESS_KEY_SECRET}
endpoint = ${OSS_ENDPOINT}
region = ${OSS_REGION}
session_token = ${ALICLOUD_SECURITY_TOKEN}
EOF
- name: Upload Windows artifacts to OSS
shell: bash
run: |
set -euo pipefail
# 同 mac 侧:include 白名单按渠道收紧,beta 发布绝不触碰 latest.yml;
# REFRESH_BETA=false 时 stable 跳过 beta.yml(防指针倒退)。
YML_INCLUDES=()
if [ "$CHANNEL" = "stable" ]; then
YML_INCLUDES+=(--include "latest.yml")
fi
if [ "$REFRESH_BETA" = "true" ]; then
YML_INCLUDES+=(--include "beta.yml")
fi
rclone --config "$HOME/.config/rclone/rclone.conf" copy \
"release/${VERSION}" "oss:${OSS_BUCKET}/${OSS_PREFIX}/win32/x64/" \
--include "Wanta-${VERSION}-Setup.exe" --include "Wanta-${VERSION}-Setup.exe.blockmap" \
"${YML_INCLUDES[@]}" --progress --retries 5 --retries-sleep 10s --low-level-retries 10
create-release:
needs: [compute-version, release-mac, release-win]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-node@v7
with:
node-version: "22"
# 同 compute-version:本 job 只跑 node scripts/release-size.ts 与 git/gh,
# 从不安装 pnpm 依赖。关掉自动 package-manager 缓存(v5 会因探测到
# pnpm-lock.yaml 却找不到 pnpm 而报错;v6+ 自动缓存只认 npm,保留防御)。
package-manager-cache: false
# 拉取各平台 build job 上传的全部 release-size-* 元数据(都是小 JSON,绝不在此
# 重新下载大安装包)。render 在缺任一必需平台元数据时快速失败。
- name: Download release size metadata
uses: actions/download-artifact@v8
with:
pattern: release-size-*
path: /tmp/release-size-metadata
merge-multiple: true
- name: Create tag and release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ needs.compute-version.outputs.version }}"
CHANNEL="${{ needs.compute-version.outputs.channel }}"
TAG="v${VERSION}"
OSS_BASE="https://static.oomol.com/${OSS_PREFIX}"
# 由 build job 元数据渲染 Downloads 表;缺元数据/版本不一致/渲染错误都会让
# release 失败——绝不发出缺了半张体积表的 Release note。
SIZE_TABLE=$(node --experimental-strip-types scripts/release-size.ts render \
--metadata-dir /tmp/release-size-metadata \
--oss-base "$OSS_BASE" \
--version "$VERSION")
# 上一个 tag 在打当前 tag 之前取,且按渠道选择:stable 对比上一个 stable
# (changelog 跨越中间所有 beta,正式版 notes 完整);beta 按时间取最近一个
# tag(增量 notes)。git 的 -v:refname 对 prerelease 后缀排序不符合 semver,
# 不能用于跨渠道比较。creatordate 只对 annotated tag 等于打 tag 时间
# (lightweight tag 会退化为所指提交的 committerdate,回溯发布时会选错上一版),
# 因此本 workflow 一律打 annotated tag(见下方 git tag -a)。
if [ "$CHANNEL" = "stable" ]; then
PREV_TAG=$(git tag -l 'v*' | grep -v -- '-' | sort -V | tail -1 || true)
else
PREV_TAG=$(git tag -l 'v*' --sort=-creatordate | head -1 || true)
fi
# annotated tag 需要 committer 身份;GitHub runner 默认未配置,
# 用 github-actions[bot] 规范身份(不污染全局,仅本仓库)。
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "$TAG"
git push origin "$TAG"
if [ -n "$PREV_TAG" ]; then
AUTO_NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="$TAG" \
-f previous_tag_name="$PREV_TAG" \
--jq '.body' 2>/dev/null || echo "")
else
AUTO_NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="$TAG" \
--jq '.body' 2>/dev/null || echo "")
fi
if [ "$CHANNEL" = "stable" ]; then
MAC_YML="latest-mac.yml"
WIN_YML="latest.yml"
else
MAC_YML="beta-mac.yml"
WIN_YML="beta.yml"
fi
{
echo "## Downloads"
echo ""
echo "$SIZE_TABLE"
echo ""
# app-downloads.oomol.com 重定向器只跟踪 stable,beta notes 不放该区块。
if [ "$CHANNEL" = "stable" ]; then
echo "## Latest Downloads"
echo ""
echo "- [Wanta macOS ARM64](https://app-downloads.oomol.com/wanta/darwin/arm64)"
echo "- [Wanta Windows x64](https://app-downloads.oomol.com/wanta/win32/x64)"
echo ""
fi
echo "## Auto Update"
echo ""
echo "| Platform | Update URL |"
echo "|----------|-----------|"
echo "| macOS | \`${OSS_BASE}/darwin/arm64/${MAC_YML}\` |"
echo "| Windows | \`${OSS_BASE}/win32/x64/${WIN_YML}\` |"
echo ""
echo "$AUTO_NOTES"
} > /tmp/release-notes.md
# beta 标记为 prerelease,绝不抢仓库 Latest;stable 显式 --latest。
if [ "$CHANNEL" = "stable" ]; then
gh release create "$TAG" --title "$TAG" --latest --notes-file /tmp/release-notes.md
else
gh release create "$TAG" --title "$TAG" --prerelease --notes-file /tmp/release-notes.md
fi
# Release 发布后刷新 release 目录的 DCDN 缓存,使自动更新客户端无需等 CDN TTL 过期即可
# 看到新的 latest-mac.yml / latest.yml。
#
# 本 job 曾复用 oomol/workflows 的共享 workflow,仓库迁到 oomol-lab 后必须内联:
# oomol/workflows 是 private 且 Actions access 为 organization 级,跨 org 调用不被允许,
# 且两个 org 不在同一 enterprise 下(oomol=team / oomol-lab=free),无法通过放宽 access 解决。
# 实现照搬 oomol-lab/oo-cli 的 .github/workflows/refresh-cdn-cache.yaml(同 org 内已验证可用)。
refresh-cdn-cache:
needs: [compute-version, create-release]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
env:
PINNED_ALIYUN_CLI_VERSION: 3.0.286
ALIYUN_REGION: cn-hangzhou
CDN_DOMAIN: static.oomol.com
# 只刷自动更新渠道指针文件(清单由 compute-version 按渠道计算:stable 刷
# latest*+beta* 共 4 个,beta 只刷 beta* 两个)。版本化产物(Wanta-${VERSION}.dmg /
# -Setup.exe 及其 .blockmap)住在每次 release 不可变的版本戳 URL 上,永不陈旧。
CDN_FILES: ${{ needs.compute-version.outputs.cdn_files }}
steps:
- name: Cache Aliyun CLI
uses: actions/cache@v6
with:
path: ~/.local/bin/aliyun
key: aliyun-cli-${{ runner.os }}-${{ runner.arch }}-${{ env.PINNED_ALIYUN_CLI_VERSION }}
- name: Install Aliyun CLI
run: |
set -euo pipefail
if [ ! -x "$HOME/.local/bin/aliyun" ]; then
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://aliyuncli.alicdn.com/aliyun-cli-linux-${PINNED_ALIYUN_CLI_VERSION}-amd64.tgz" -o /tmp/aliyun-cli.tgz
mkdir -p /tmp/aliyun-cli
tar -xzf /tmp/aliyun-cli.tgz -C /tmp/aliyun-cli
ALIYUN_BIN="$(find /tmp/aliyun-cli -type f -name aliyun | head -n 1)"
if [ -z "$ALIYUN_BIN" ]; then
echo "::error::Unable to find aliyun binary in downloaded archive"; exit 1
fi
install "$ALIYUN_BIN" "$HOME/.local/bin/aliyun"
fi
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/aliyun" version
- name: Configure Alibaba Cloud credentials
id: aliyun_credentials
uses: aliyun/configure-aliyun-credentials-action@v1.1.0
with:
role-to-assume: ${{ vars.ALIYUN_CDN_CACHE_REFRESH_ROLE }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER }}
role-session-name: github-action-cdn-cache-refresh
role-session-expiration: 1800
audience: https://github.com/oomol-lab
- name: Configure Aliyun CLI
env:
ALICLOUD_ACCESS_KEY_ID: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-id }}
ALICLOUD_ACCESS_KEY_SECRET: ${{ steps.aliyun_credentials.outputs.aliyun-access-key-secret }}
ALICLOUD_SECURITY_TOKEN: ${{ steps.aliyun_credentials.outputs.aliyun-security-token }}
run: |
set -euo pipefail
aliyun configure set \
--mode StsToken \
--region "$ALIYUN_REGION" \
--access-key-id "$ALICLOUD_ACCESS_KEY_ID" \
--access-key-secret "$ALICLOUD_ACCESS_KEY_SECRET" \
--sts-token "$ALICLOUD_SECURITY_TOKEN"
- name: Refresh CDN cache
run: |
set -euo pipefail
# CDN_FILES 是 compute-version 产出的多行绝对路径清单;拼上域名成完整 URL。
# 空清单说明 outputs plumbing 回归(拼错输出名→空串),必须大声失败而不是静默跳过刷新。
URLS=""
COUNT=0
while IFS= read -r FILE; do
[ -n "$FILE" ] || continue
URLS="${URLS}https://${CDN_DOMAIN}${FILE}"$'\n'
COUNT=$((COUNT + 1))
done <<< "$CDN_FILES"
if [ "$COUNT" -eq 0 ]; then
echo "::error::CDN_FILES is empty — nothing to refresh (channel pointer plumbing regression?)"
exit 1
fi
aliyun dcdn DescribeDcdnRefreshQuota --region "$ALIYUN_REGION" > quota.json
cat quota.json
URL_REMAIN="$(jq -r '.UrlRemain // empty' quota.json)"
if [ -n "$URL_REMAIN" ] && [ "$URL_REMAIN" -lt "$COUNT" ]; then
echo "::error::DCDN URL refresh quota insufficient (UrlRemain=$URL_REMAIN, need=$COUNT). Retry tomorrow or raise the daily quota via an Aliyun ticket."
exit 1
fi
echo "::notice title=Refresh target::${COUNT} channel pointer file(s)"
printf '%s' "$URLS"
# RefreshDcdnObjectCaches 的 ObjectPath 支持换行分隔的多 URL,一次提交即可。
aliyun dcdn RefreshDcdnObjectCaches \
--ObjectType File \
--ObjectPath "$(printf '%s' "$URLS")" \
--region "$ALIYUN_REGION" > refresh-result.json
cat refresh-result.json
REFRESH_TASK_ID="$(jq -r '.RefreshTaskId // ""' refresh-result.json)"
REQUEST_ID="$(jq -r '.RequestId // ""' refresh-result.json)"
{
echo "## CDN cache refresh"
echo ""
echo "- Refreshed ${COUNT} file(s):"
printf '%s' "$URLS" | sed 's/^/ - `/; s/$/`/'
echo "- Refresh task id: \`${REFRESH_TASK_ID:-unknown}\`"
echo "- Request id: \`${REQUEST_ID:-unknown}\`"
echo ""
echo "_DCDN refresh tasks typically take 5-6 minutes to take effect at every edge node._"
} >> "$GITHUB_STEP_SUMMARY"