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
# 打 tag 即发版:tag 名当版本号(build.rs 用 `git describe --tags --exact-match` 注入,
# `jscd --version` / `jscd version` 直接打印它),构建六个平台的裸二进制并挂到 Release。
#
# 打 tag 用 `scripts/release.sh vX.Y.Z` —— 它会先跑本地门禁,再打一个**带说明**的
# annotated tag;发版说明优先取 tag 自己的说明(没写就退回 .github/release-body.md),
# 前置到 GitHub 自动生成的 release notes 之上,见下面的 "Compose release body"。
#
# 产物不做二次压缩(不是 zip/gz/tar)—— 直接是各平台的可执行文件本身。
#
# UPX:Linux / Windows 四个产物压缩;macOS **不压**(UPX 对 Mach-O 会直接拒绝:
# "macOS is currently not supported",写了 --force-macos 压出来的产物也不可信)。
# 压缩步骤是"尽力而为":upx 失败/不支持时保留未压缩产物并继续,不让发版挂掉。
name: release
on:
push:
tags: # 任意 tag(v1.2.3、1.2.3、jscd-1.2.3…都命中,不含 / 的写法)
workflow_dispatch: # 手动跑一次演练(版本号会退化成 "dev")
permissions:
contents: write # 创建 Release / 上传资产
jobs:
# 发版前的最低门禁:单测。不通过就不构建产物。
checks:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # build.rs 要能 describe 到 tag
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
# build.rs 把 tag 名注入 `jscd --version`;tag 必须与 Cargo.toml 的版本一致,
# 否则二进制报的版本和包元数据(crates.io / cargo install)对不上。
- name: Tag matches Cargo.toml
if: github.ref_type == 'tag'
shell: bash
run: |
pkg_ver="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)"
echo "tag=${GITHUB_REF_NAME} Cargo.toml=${pkg_ver}"
test "${GITHUB_REF_NAME}" = "v${pkg_ver}"
- name: cargo test
run: cargo test --release
# 项目的质量口径是"零告警":有告警就不发版。
- name: cargo clippy
run: cargo clippy --all-targets -- -D warnings
# 端到端冒烟:用固定大版本的 node(同一条 node 线里 V8 minor 不变,保证表的命中稳定)
# 把仓库里的 fixture 真编译成 .jsc → 反编译 → 语法门禁 + 整脚本行为对拍。
- uses: actions/setup-node@v4
with:
node-version: 20
- name: cargo build --release
run: cargo build --release
- name: end-to-end smoke (compile -> decompile -> run)
run: scripts/ci_smoke.sh
build:
needs: checks
name: ${{ matrix.asset }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux x64:静态 musl(musl-tools 提供 musl-gcc 作链接器)
- os: ubuntu-24.04
target: x86_64-unknown-linux-musl
asset: linux-amd64
ext: ""
upx: true
apt: musl-tools
linker_env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER
linker: musl-gcc
# Linux arm64:交叉编译到 gnu(gcc-aarch64-linux-gnu 当链接器)
- os: ubuntu-24.04
target: aarch64-unknown-linux-gnu
asset: linux-arm64
ext: ""
upx: true
apt: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
# macOS arm64:原生构建;UPX 不支持 Mach-O → 不压
- os: macos-14
target: aarch64-apple-darwin
asset: macos-arm64
ext: ""
upx: false
apt: ""
linker_env: ""
linker: ""
# macOS x64:在 arm64 runner 上交叉到 x86_64(Apple SDK 双向可用);同样不压
- os: macos-14
target: x86_64-apple-darwin
asset: macos-amd64
ext: ""
upx: false
apt: ""
linker_env: ""
linker: ""
# Windows x64
- os: windows-latest
target: x86_64-pc-windows-msvc
asset: windows-amd64
ext: ".exe"
upx: true
apt: ""
linker_env: ""
linker: ""
# Windows arm64:在 x64 runner 上交叉。rustc 按 MSVC 目标调 ARM64 的 link.exe,
# windows-latest 的 VS 里带 ARM64 构建工具;若某天镜像去掉该组件,
# 换成 `runs-on: windows-11-arm` 原生构建即可。
- os: windows-latest
target: aarch64-pc-windows-msvc
asset: windows-arm64
ext: ".exe"
upx: true
apt: ""
linker_env: ""
linker: ""
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install cross linker (Linux)
if: matrix.os == 'ubuntu-24.04'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ${{ matrix.apt }}
- name: Build
shell: bash
env:
LINKER_ENV: ${{ matrix.linker_env }}
LINKER: ${{ matrix.linker }}
run: |
if [ -n "$LINKER_ENV" ]; then
export "$LINKER_ENV=$LINKER"
fi
cargo build --release --target "${{ matrix.target }}"
# 版本号 = tag 名(与 build.rs 注入 CLI 的值一致)
- name: Collect artifact
shell: bash
run: |
VERSION="${GITHUB_REF_NAME:-dev}"
SRC="target/${{ matrix.target }}/release/jscd${{ matrix.ext }}"
OUT="jscd-${VERSION}-${{ matrix.asset }}${{ matrix.ext }}"
mkdir -p dist
cp "$SRC" "dist/$OUT"
echo "OUT=dist/$OUT" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
ls -l "dist/$OUT"
# UPX:仅 Linux / Windows;失败就保留未压缩产物(不 fail 发版)
- name: UPX (Linux)
if: matrix.upx && matrix.os == 'ubuntu-24.04'
shell: bash
run: |
sudo apt-get install -y upx-ucl
upx --best --lzma "$OUT" || echo "upx 未压缩(保留原产物)"
- name: UPX (Windows)
if: matrix.upx && startsWith(matrix.os, 'windows')
shell: pwsh
run: |
choco install upx --no-progress -y
upx --best --lzma $env:OUT
if ($LASTEXITCODE -ne 0) { Write-Host "upx 未压缩(保留原产物)" }
exit 0 # upx 缺席/失败都不算这一步失败
- name: Show final size
shell: bash
run: ls -l "$OUT"
# 发版说明(Release 描述):
# 1) 优先用 **annotated tag 的说明** —— `scripts/release.sh` 打的 tag 自带描述;
# 2) 轻量 tag / 没写说明的 tag → 退回仓库模板 .github/release-body.md,
# 把 @VERSION@ 换成 tag 名。
# 用 `%(contents:subject)` + `%(contents:body)` 而不是 `%(contents)`:本仓库的
# tag 是自动签名的(tag.gpgSign=true),`%(contents)` 会把整段 SSH/GPG 签名块
# 也带进 Release 描述,而 subject/body 两个 atom 不含签名。
# 描述由 action-gh-release 前置到 GitHub 自动生成的 release notes 之上。
- name: Compose release body
if: github.ref_type == 'tag'
shell: bash
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
subject=""
body=""
if [ "$(git cat-file -t "refs/tags/$tag" 2>/dev/null || echo none)" = "tag" ]; then
subject="$(git tag -l --format='%(contents:subject)' "$tag" 2>/dev/null || true)"
body="$(git tag -l --format='%(contents:body)' "$tag" 2>/dev/null || true)"
fi
if [ -n "$subject$body" ]; then
echo "release body: 取自 annotated tag 的说明(已去掉签名块)"
{
printf '%s\n' "$subject"
if [ -n "$body" ]; then printf '\n%s\n' "$body"; fi
} > release-body.md
else
echo "release body: tag 没有说明,退回 .github/release-body.md 模板"
sed "s/@VERSION@/$tag/g" .github/release-body.md > release-body.md
fi
head -3 release-body.md
# 直接以裸文件挂到 Release(不经 upload-artifact —— 它一定会打成 zip)。
# 多个矩阵任务会同时写同一个 Release:action-gh-release 会先查后建,
# 并发窗口极小且上传按资产名独立,实践中稳定。
- name: Upload to release
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: jscd ${{ github.ref_name }}
body_path: release-body.md
generate_release_notes: true
fail_on_unmatched_files: true
files: ${{ env.OUT }}
# 手动触发时没有 tag、也就没有 Release → 给一份 workflow 产物方便检查。
# 这只是演练路径:发版的产物一律走上面的 Release(裸文件)。
- name: Upload as workflow artifact (manual dry-run only)
if: github.ref_type != 'tag'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: ${{ env.OUT }}
compression-level: 0 # 容器仍是 zip(仅手动演练用),但不做额外压缩