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
# Release workflow(仅 [publish] 关键字或手动触发才执行):
# · push 到 main:普通提交不执行任何 release 动作(不 bump、不打 tag、不出 Release);
# 提交信息含 "[publish]" 关键字时:bump 版本 + 打 tag + GitHub Release + 发布 crates.io + npm stage publish。
# · 手动触发(workflow_dispatch):可指定 minor / major,并可分别勾选是否发布 crates.io / npm。
# · npm 采用 stage 模式 + Trusted Publishing(OIDC,无需 NPM_TOKEN):
# 需先在 npmjs → Trusted Publishing 配置 GitHub Environment "npm"(owner=w916797724, repo=multiavatar-rust);
# CI 只把版本推入暂存区(npm stage publish),正式发布需 maintainer(2FA)promote:
# 网页 https://www.npmjs.com/package/@w916797724/multiavatar → Staged releases → Approve
# 命令行 npm stage promote --package @w916797724/multiavatar --version <版本>
# 版本递增提交带 [skip ci],避免自身提交再次触发本 workflow。
name: Release
on:
push:
branches:
paths:
- "src/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/release.yml"
workflow_dispatch:
inputs:
version_bump:
description: "版本递增类型(手动触发时选择;push [publish] 触发时固定为 patch)"
type: choice
default: patch
options:
- patch
- minor
- major
publish_crates:
description: "发布 crates.io(需 secret CARGO_REGISTRY_TOKEN)"
type: boolean
default: true
publish_npm:
description: "npm stage publish(Trusted Publishing/OIDC,正式发布需手动 promote)"
type: boolean
default: true
permissions:
contents: write
id-token: write # Trusted Publishing(OIDC)必需:npm 借此验证 workflow 身份
jobs:
release:
runs-on: ubuntu-24.04
environment: npm # 与 npmjs Trusted Publishing 配置的 GitHub Environment 同名
# 门控:只有手动触发,或 push 的最近提交含 "[publish]" 才进入 release 流程
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && contains(github.event.head_commit.message, '[publish]'))
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# ---- 1. 递增版本号(纯 bash,无 heredoc/python,避免引号转义坑)----
- name: Bump version
id: next_version
run: |
KIND="${{ github.event.inputs.version_bump || 'patch' }}"
[ -z "$KIND" ] && KIND=patch
VER="$(grep -m1 '^version' Cargo.toml | cut -d '"' -f2)"
IFS=. read -r MAJ MIN PAT <<< "$VER"
if [ "$KIND" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0
elif [ "$KIND" = "minor" ]; then MIN=$((MIN+1)); PAT=0
else PAT=$((PAT+1)); fi
NV="$MAJ.$MIN.$PAT"
sed -i "s/^version = .*/version = \"$NV\"/" Cargo.toml
# 同步 npm 包版本(单一来源:Cargo.toml)
if [ -f wasm/package.json ]; then
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$NV\"/" wasm/package.json
fi
cargo metadata --no-deps --format-version 1 >/dev/null
echo "version=$NV" >> "$GITHUB_OUTPUT"
# ---- 2. 打包校验 + 发布 crates.io / npm ----
# 发布条件(三者缺一不可):
# ① 对应 secret(CARGO_REGISTRY_TOKEN)已配置(npm 走 Trusted Publishing/OIDC,无需 token);
# ② 手动触发(workflow_dispatch)且勾选对应开关;
# 或 push 触发且最近提交信息含 "[publish]" 关键字(两个仓库同时发);
# ③ cargo package 校验通过(crates.io 步骤天然保证)。
- name: Package verify
run: cargo package --allow-dirty
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# --allow-dirty:版本号已由 Bump 修改但 Commit and tag 在全部发布成功后才执行(设计使然)
run: cargo publish --registry crates-io --allow-dirty
if: |
env.CARGO_REGISTRY_TOKEN != '' &&
(
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_crates == 'true') ||
(github.event_name == 'push' && contains(github.event.head_commit.message, '[publish]'))
)
# npm 包(stage 模式 + Trusted Publishing):wasm/package.json 版本已由 Bump 步骤同步。
# 无需 NPM_TOKEN——npm 通过 GitHub OIDC(id-token: write + environment: npm)验证身份;
# --provenance 附加来源证明。npm stage publish 是 npm 12+ 命令(CI 镜像 npm 10/11 没有),
# 故用 npx npm@12 显式调用;prepublishOnly(node 同步脚本)幂等无害。
- name: Stage npm package
working-directory: wasm
run: |
npx -y npm@12 stage publish --registry https://registry.npmjs.org --provenance
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_npm == 'true') ||
(github.event_name == 'push' && contains(github.event.head_commit.message, '[publish]'))
# ---- 4. 构建可运行 demo 二进制并打 Release(附 .crate 源码包)----
# 注意:不带 --locked——Cargo.lock 被 .gitignore 忽略,CI 检出时不存在
- name: Build demo binary
run: cargo build --release --example demo
# ---- 5. 全部发布成功后:提交版本变更 + 打 tag([skip ci] 防循环)----
# tag 只代表"已发布的版本":crates.io / npm stage 均成功后才落 tag;
# 发布失败则工作区无 git 痕迹,重跑自动跳到下一版本号。
- name: Commit and tag
if: steps.next_version.outputs.version != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Cargo.toml
git add wasm/package.json 2>/dev/null || true
# Cargo.lock 被 .gitignore 忽略(库 crate 惯例,不随 checkout 存在):
# 存在才 add(cargo metadata 会在本地生成它),不存在则跳过
git add Cargo.lock 2>/dev/null || true
git commit -m "release: v${{ steps.next_version.outputs.version }} [skip ci]"
git tag "v${{ steps.next_version.outputs.version }}"
git push origin HEAD:main
git push origin "v${{ steps.next_version.outputs.version }}"
# ---- 6. 打 GitHub Release(需 tag 已 push 到远端)----
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v${{ steps.next_version.outputs.version }}" \
--repo "${{ github.repository }}" \
--title "v${{ steps.next_version.outputs.version }}" \
--generate-notes \
target/package/multiavatar-*.crate \
target/release/examples/demo