Repository navigation
338 lines (312 loc) · 15.9 KB
/
Copy pathplugin-publish.yml
File metadata and controls
338 lines (312 loc) · 15.9 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
# EasyBot 插件发布模板(自包含,copy 到插件仓库即可用)
#
# 用法:
# 1. 本机:easybot-plugin-sign gen-keypair 生成 ed25519 密钥对
# 2. 私钥存入插件仓库的 GitHub Actions secret `PUBLISHER_PRIVATE_KEY`
# 3. 公钥(PUBLIC_KEY 那行)通过 PR 提交给 EasyBot 官方登记 trusted_publishers,
# 登记后客户端才能验签安装(签名只证作者与完整性,不证安全)
# 4. 按需修改下方 env(CHANNEL / EASYBOT_REQUIRES / SDK_VERSION / SDK_TAG)
# 5. 打 tag v1.0.0 推送 → 自动交叉编译 6 target + gitleaks 扫描 + 签名 + 发布
#
# 约束:
# - 本模板是「自包含」的——只能引用公开 GitHub Action(并固定 40 位 commit SHA),
# 不能引用 EasyBot 仓库的内部 action(插件作者仓库里没有)。
# - 产物命名:{name}-{triple}[.exe](插件是独立可执行文件,无 lib 前缀)。
# 同一 Release 下 6 个产物用 triple 后缀区分,安装端按宿主 triple 只下载匹配项。
# - 宿主与插件是「进程外」关系(stdio JSON 协议),因此不要求单个目标静态链接:
# 宿主即使全静态(无动态加载器、无法 dlopen)也能运行插件。
# 仍固定 6 个 target triple,保证同一 Release 覆盖所有平台。
# - sdkVersion 必须等于插件所依赖 SDK 的 EASYBOT_PLUGIN_ABI_VERSION(当前为 1)。
#
# v1.5 可选:在「Release」step 前追加 actions/attest@v4 为产物生成 GitHub
# provenance(不改变 ed25519 信任锚,仅追加发布可溯源性;需 id-token: write)。
name: Publish Plugin
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 插件版本(如 1.2.3;留空则取 git tag 或 Cargo.toml [package].version)
required: false
default: ''
env:
# ── 发布者配置(按需修改) ──
# 插件名:留空则从 Cargo.toml [package].name 读取
PLUGIN_NAME: ""
# 发布渠道:stable | beta(beta 的 Release 标记为 prerelease)
CHANNEL: "stable"
# requires.easybot:兼容的最低 EasyBot 版本(semver range,客户端安装时校验)
EASYBOT_REQUIRES: ">=0.0.28"
# sdkVersion:必须与插件所依赖 SDK 的 EASYBOT_PLUGIN_ABI_VERSION 一致(当前为 1)。
# 更换 SDK git tag(ABI 变更)时必须同步更新。
SDK_VERSION: "1"
# SDK git 依赖 tag(形如 v0.0.34,用于安装 easybot-plugin-sign)。
# 留空则从 Cargo.toml 的 easybot-plugin-sdk 依赖自动读取。
# 注意:该 tag 对应的 EasyBot 版本必须已包含 easybot-plugin-sign 工具。
SDK_TAG: ""
permissions:
contents: read
jobs:
# ── 0. 守卫:本模板是给插件仓库 copy 用的,只在真正的插件工程里运行 ──
# 主仓 EasyIndie/EasyBot 的根 Cargo.toml 是 `[workspace]`(无 `[package]`),
# 若不加守卫,每次打 tag 都会在此触发一次无意义的 6-target 构建并失败。
# 结构判断而非硬编码仓库名:任何"根 Cargo.toml 无 [package]"的非插件仓库也会被跳过。
guard:
name: Guard (plugin project?)
runs-on: ubuntu-latest
outputs:
is_plugin: ${{ steps.check.outputs.is_plugin }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- id: check
run: |
if grep -q '^\[package\]' Cargo.toml 2>/dev/null; then
echo "is_plugin=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::根 Cargo.toml 无 [package],非插件工程;跳过 Publish Plugin 工作流。"
echo "is_plugin=false" >> "$GITHUB_OUTPUT"
fi
# ── 1. 交叉编译 6 target(公开 action,不引用仓库内部 action) ──
build:
name: Build ${{ matrix.target }}
needs: guard
if: needs.guard.outputs.is_plugin == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux:musl(zig cc 交叉编译,静态;产物是独立进程,静态无副作用)
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
ext: ""
zigbuild: true
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
ext: ""
zigbuild: true
# macOS:部署目标 ≤ 宿主(10.15 / 11.0)
- os: macos-latest
target: x86_64-apple-darwin
ext: ""
zigbuild: false
macos-deployment-target: "10.15"
- os: macos-latest
target: aarch64-apple-darwin
ext: ""
zigbuild: false
macos-deployment-target: "11.0"
# Windows:MSVC CRT;ARM 用原生 windows-11-arm runner
- os: windows-latest
target: x86_64-pc-windows-msvc
ext: ".exe"
zigbuild: false
- os: windows-11-arm
target: aarch64-pc-windows-msvc
ext: ".exe"
zigbuild: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # v1
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install cargo-zigbuild (musl)
if: ${{ matrix.zigbuild }}
uses: taiki-e/install-action@4076c08d76dba979c11a7285295b0716c1d67908 # v2
with:
tool: cargo-zigbuild
# cargo-zigbuild 0.19+ 不再捆绑 zig,需单独安装(否则报 "Failed to find zig")。
# 直接下载 ziglang.org 官方稳定版(自包含,不依赖 install-action 对 zig 的支持版本)。
# 两个 musl target 都在 ubuntu-latest (x86_64) 上构建,共用同一份 zig。
# 注意:必须整目录解压后把目录加入 PATH——zig 的 `zig cc` 需要相邻的 lib/ 目录
# 定位 std 库;只拷 zig 二进制会报 "unable to find zig installation directory"。
- name: Install zig (required by cargo-zigbuild)
if: ${{ matrix.zigbuild }}
run: |
set -euo pipefail
curl -fsSL --retry 5 --retry-connrefused -o /tmp/zig.tar.xz \
https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
mkdir -p "$HOME/zig"
tar -xJf /tmp/zig.tar.xz -C "$HOME/zig"
# GITHUB_PATH 只在下一步生效;本步内要立刻用 zig,需同时 export PATH
echo "$HOME/zig/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"
export PATH="$HOME/zig/zig-x86_64-linux-0.16.0:$PATH"
zig version
- name: Resolve plugin name
id: name
shell: bash
run: |
set -euo pipefail
PLUGIN_NAME="${PLUGIN_NAME:-$(awk -F'"' '/^\[package\]/{p=1} p&&/^name *=/{gsub(/ /,"");print $2;exit}' Cargo.toml)}"
[ -n "$PLUGIN_NAME" ] || { echo "无法确定插件名(设置 env.PLUGIN_NAME 或 Cargo.toml [package].name)" >&2; exit 1; }
echo "name=$PLUGIN_NAME" >> "$GITHUB_OUTPUT"
- name: Build (musl)
if: ${{ matrix.zigbuild }}
run: cargo zigbuild --release --target ${{ matrix.target }} --bin ${{ steps.name.outputs.name }}
- name: Build (native)
if: ${{ !matrix.zigbuild }}
env:
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos-deployment-target }}
run: cargo build --release --target ${{ matrix.target }} --bin ${{ steps.name.outputs.name }}
- name: Stage plugin artifacts
# Windows runner 默认 shell 是 PowerShell,必须显式用 bash(脚本是 bash 语法)
shell: bash
env:
EXT: ${{ matrix.ext }}
TRIPLE: ${{ matrix.target }}
run: |
set -euo pipefail
# 插件名(缺省从 Cargo.toml [package].name 读取)
PLUGIN_NAME="${PLUGIN_NAME:-$(awk -F'"' '/^\[package\]/{p=1} p&&/^name *=/{gsub(/ /,"");print $2;exit}' Cargo.toml)}"
[ -n "$PLUGIN_NAME" ] || { echo "无法确定插件名(设置 env.PLUGIN_NAME 或 Cargo.toml [package].name)" >&2; exit 1; }
# 二进制产物名 = bin 名(连字符保留),Windows 带 .exe
CLEAN="${PLUGIN_NAME}${EXT}"
RAW="target/${TRIPLE}/release/${CLEAN}"
[ -f "$RAW" ] || { echo "未找到编译产物: $RAW" >&2; exit 1; }
# Release asset 名:{name}-{triple}[.exe]
SUFFIXED="${PLUGIN_NAME}-${TRIPLE}${EXT}"
mkdir -p artifacts
cp "$RAW" "artifacts/${SUFFIXED}"
printf '{"triple":"%s","command":"%s","suffixed":"%s"}' \
"$TRIPLE" "$CLEAN" "$SUFFIXED" > artifacts/metadata.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
with:
name: artifact-${{ matrix.target }}
path: artifacts/
if-no-files-found: error
# ── 2. secret 扫描(VS Code 教训:含密钥即阻断发布) ──
# 用 gitleaks 二进制直接跑(免 GITLEAKS_LICENSE 授权):gitleaks-action 较新版本
# 强制要求付费 license,自包含模板不能依赖外部 secret。主仓(非插件工程)经
# guard 判定后跳过本 job,避免每次打 tag 都被误触发。
secret-scan:
name: Secret scan
runs-on: ubuntu-latest
needs: guard
if: needs.guard.outputs.is_plugin == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install gitleaks (license-free binary)
run: |
set -euo pipefail
curl -fsSL --retry 5 --retry-connrefused -o /tmp/gitleaks.tar.gz \
https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks
chmod +x /usr/local/bin/gitleaks
gitleaks version
- name: Gitleaks scan
run: gitleaks detect --source . --redact
# ── 3. 签名 + 组装 easybot-plugin.json + 发布 ──
publish:
name: Sign and release
runs-on: ubuntu-latest
needs: [build, secret-scan]
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Resolve version, publisher & SDK tag
id: meta
run: |
set -euo pipefail
PLUGIN_NAME="${PLUGIN_NAME:-$(awk -F'"' '/^\[package\]/{p=1} p&&/^name *=/{gsub(/ /,"");print $2;exit}' Cargo.toml)}"
VERSION="${{ inputs.version }}"
if [ -z "$VERSION" ]; then
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ]; then
VERSION=$(awk -F'"' '/^\[package\]/{p=1} p&&/^version *=/{gsub(/ /,"");print $2;exit}' Cargo.toml)
fi
[ -n "$VERSION" ] || { echo "无法确定版本(workflow_dispatch 传 version,或打 vX.Y.Z tag)" >&2; exit 1; }
TAG="v${VERSION}"
SDK_TAG="${SDK_TAG:-$(grep 'easybot-plugin-sdk' Cargo.toml | grep -oE '"[vV]?[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | head -1)}"
[ -n "$SDK_TAG" ] || { echo "无法从 Cargo.toml 读取 SDK tag(SDK_TAG 需为 vX.Y.Z 形式)" >&2; exit 1; }
echo "plugin-name=${PLUGIN_NAME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "sdk-tag=${SDK_TAG}" >> "$GITHUB_OUTPUT"
echo "publisher=${GITHUB_REPOSITORY_OWNER}" >> "$GITHUB_OUTPUT"
- name: Install easybot-plugin-sign
# 必须是 block scalar(`run: |`)——plain scalar 会把换行折叠成空格,
# 使 `\ --tag` 变成单个词 ` --tag`,cargo 误判为包名而报错。
run: |
cargo install --git https://github.com/EasyIndie/EasyBot.git \
--tag "${{ steps.meta.outputs.sdk-tag }}" --locked easybot-plugin-sign
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4
with:
path: downloads
pattern: artifact-*
- name: Sign artifacts (ed25519)
env:
PUBLISHER_PRIVATE_KEY: ${{ secrets.PUBLISHER_PRIVATE_KEY }}
run: |
set -euo pipefail
[ -n "$PUBLISHER_PRIVATE_KEY" ] || { echo "缺少 secret PUBLISHER_PRIVATE_KEY" >&2; exit 1; }
for dir in downloads/artifact-*/; do
triple=$(jq -r .triple "$dir/metadata.json")
command=$(jq -r .command "$dir/metadata.json")
suffixed=$(jq -r .suffixed "$dir/metadata.json")
easybot-plugin-sign sign --publisher "${{ steps.meta.outputs.publisher }}" \
--version "${{ steps.meta.outputs.version }}" \
--triple "$triple" --artifact "$dir/$suffixed" --library "$command" \
--name "${{ steps.meta.outputs.plugin-name }}" \
> "$dir/sign.json"
# 注入 release asset 名,供组装 easybot-plugin.json 的 url 字段
jq --arg suffixed "$suffixed" '. + {suffixed: $suffixed}' \
"$dir/sign.json" > "$dir/sign.tmp" && mv "$dir/sign.tmp" "$dir/sign.json"
done
# 断言所有产物的公钥一致(防止密钥配置错误导致部分产物无法验签)
first_pk=$(jq -r .public_key downloads/artifact-*/sign.json | head -1)
for f in downloads/artifact-*/sign.json; do
[ "$(jq -r .public_key "$f")" = "$first_pk" ] || { echo "产物公钥不一致: $f" >&2; exit 1; }
done
- name: Assemble easybot-plugin.json + checksums.txt
run: |
set -euo pipefail
mkdir -p release-assets
entries=()
for dir in downloads/artifact-*/; do
suffixed=$(jq -r .suffixed "$dir/metadata.json")
cp "$dir/$suffixed" release-assets/
entries+=("$(cat "$dir/sign.json")")
done
entries_json=$(printf '%s\n' "${entries[@]}" | jq -s .)
jq -n \
--argjson e "$entries_json" \
--arg name "${{ steps.meta.outputs.plugin-name }}" \
--arg version "${{ steps.meta.outputs.version }}" \
--argjson sdkVersion "$SDK_VERSION" \
--arg publisher "${{ steps.meta.outputs.publisher }}" \
--arg tag "${{ steps.meta.outputs.tag }}" \
--arg channel "$CHANNEL" \
--arg requires "$EASYBOT_REQUIRES" \
--arg base "https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ steps.meta.outputs.tag }}" \
'{ schemaVersion: 1, name: $name, version: $version, sdkVersion: $sdkVersion,
publisher: $publisher, tag: $tag, channel: $channel,
requires: { easybot: $requires }, deprecated: false,
artifacts: ( $e | map( { (.triple): { url: ($base + "/" + .suffixed),
size: .size, sha256: .sha256,
signature: .signature, public_key: .public_key,
command: .library } } ) | add ) }' \
> easybot-plugin.json
jq -e '.artifacts | length > 0' easybot-plugin.json >/dev/null
( cd release-assets && sha256sum * | sort ) > checksums.txt
- name: Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
name: ${{ steps.meta.outputs.plugin-name }} v${{ steps.meta.outputs.version }}
body: |
EasyBot 插件 `${{ steps.meta.outputs.plugin-name }}` v${{ steps.meta.outputs.version }}
- channel: `${{ env.CHANNEL }}`
- requires.easybot: `${{ env.EASYBOT_REQUIRES }}`
- sdkVersion: `${{ env.SDK_VERSION }}`
所有产物均经过 ed25519 签名。客户端安装时校验 sha256 + 签名,
只有通过校验才会落位加载。签名只证明作者与完整性,不证明代码安全。
files: |
easybot-plugin.json
checksums.txt
release-assets/*
prerelease: ${{ env.CHANNEL == 'beta' }}