Repository navigation
256 lines (253 loc) · 15.4 KB
/
Copy pathtest.yml
File metadata and controls
256 lines (253 loc) · 15.4 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
name: Test
on:
push:
branches: [master]
# No `branches` filter on purpose: a base branch can be any branch. Filtering to
# master gave the stacked PR #1148 (base = a feature branch) **zero** check runs,
# while its base-on-master parent #1147 was double-green automatically - the gate
# only ran when someone remembered `gh workflow run test.yml`, and the
# check-run-based vote helper reads a missing check as "no vote" rather than as a
# failure. Covered by tests/test_test_workflow_covers_pr_bases.py.
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# #441 教训:workflow 文件的 if: secrets 等解析错误人工审查易漏
# (actionlint 可捕获),push 后才在 build-release 触发时暴露。
# → CI 全量校验所有 workflow,解析错误立即失败(R82 固化)。
# The pinned release binary, not `rhysd/actionlint@v1.7.12`: that action builds its
# own image at run time, which pulls alpine, golang and shellcheck-alpine from
# `registry-1.docker.io` anonymously. Docker Hub rate-limits those per IP and the
# hosted runners share addresses, so when the limit is hit this leg dies in ~20 s
# before a single test runs (measured 2026-10-09: runs 37990298947, 37990543473,
# 37992404489 and the master push 37992520930, all at this step; issue #2019). A red
# leg is not votable, so one throttle window freezes every open PR at once while
# master reads red for no commit's fault. Same tool, same version, no registry.
- name: Actionlint (workflow lint)
env:
# The pin, and the only copy of it: `scripts/check-workflows.py` reads it out of
# this file (the host-side counterpart), and tests/test_check_workflows.py
# asserts it stays findable there.
ACTIONLINT_VERSION: "1.7.12"
run: |
set -euo pipefail
url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -fsSL "$url" | tar -xz -C "$RUNNER_TEMP" actionlint
bin="$RUNNER_TEMP/actionlint"
# The container this replaced bundled shellcheck; the release binary does not, and
# actionlint runs shellcheck over `run:` blocks only when it finds one on PATH. So
# installing the linter is not the same as keeping that half of the gate: a gate
# that quietly stopped running it would keep the name while checking less. Both
# halves are asserted below -- the binary answers, and it still reports a finding
# that only the linter can supply (never write a comment line here that begins with
# the word `# shellcheck`: this file's prose is shell to the linter, and a line
# shaped like its directive is a parse error, measured 2026-10-10).
command -v shellcheck >/dev/null || (sudo apt-get update && sudo apt-get install -y shellcheck)
command -v shellcheck >/dev/null
"$bin" -version
probe="$(mktemp -d)"
mkdir -p "$probe/.github/workflows"
cat > "$probe/.github/workflows/probe.yml" <<'PROBE'
name: probe
on: push
jobs:
p:
runs-on: ubuntu-latest
steps:
- run: echo $UNQUOTED
PROBE
# The path is explicit on purpose. With no argument actionlint lints the nearest
# *repository's* `.github/workflows`, which from a temporary directory is the
# checkout that contains it - an answer about this tree, not about the probe
# (measured on this host with 1.7.12: the same file answers clean by that route
# and `SC2086` by this one). The lint below passes its files for the same reason.
if (cd "$probe" && "$bin" .github/workflows/probe.yml >/dev/null 2>&1); then
echo "::error::actionlint found nothing in a workflow with an unquoted expansion - the shellcheck half of this gate is not running"
exit 1
fi
files=()
while IFS= read -r f; do files+=("$f"); done < <(find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort)
if [ "${#files[@]}" -eq 0 ]; then
echo "::error::no workflow file under .github/workflows, so nothing was linted"
exit 1
fi
"$bin" "${files[@]}"
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.13"
# The Linux sandbox dialect's runner. `test_bwrap_really_binds_a_named_file_writable`
# measures a single-file extra root by *running* bwrap rather than by asserting its
# argv (rant 2026-10-09T09:43:39 §7), and that test skips where the binary is
# absent - so without this step the leg would report a skip and measure nothing.
- name: Ensure bubblewrap (the Linux sandbox dialect's runner)
run: |
command -v bwrap >/dev/null || (sudo apt-get update && sudo apt-get install -y bubblewrap)
# Installing it is not enough. Ubuntu 23.10+ restricts **unprivileged user
# namespaces** through AppArmor, and bwrap needs one before it can start any
# command; with the restriction on, every invocation dies with
# bwrap: setting up uid map: Permission denied
# The first run of this leg did exactly that (run 37943772666), and the failure
# is the host refusing to create the namespace, not the argv under test being
# wrong. The runner is an ephemeral VM, so lifting it here is scoped to this job.
# A runner that has no such knob is not broken - older kernels allow the
# namespace outright - so the knob is read and reported, never assumed.
if [ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
echo "apparmor_restrict_unprivileged_userns: $(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns) -> 0"
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
# Then assert the runner can run what it just installed: a leg that installs
# bwrap and then skips the measurement is the shape this PR exists to remove, so
# if bwrap still cannot start, this step fails here with bwrap's own words rather
# than letting the suite report a green skip.
bwrap --ro-bind / / -- true
echo "bwrap runs: the Linux cases below measure rather than skip."
- run: uv sync
# `-n auto --dist loadfile` (issue #2296 R1/R2): the suite is dominated by
# subprocess-spawning tests, so wall time is bought back with workers. `loadfile`
# rather than xdist's default `load`, because a whole-tree guard's verdict is cached
# per session (`run_guard_once` in tests/conftest.py) and one file's legs must land
# on one worker for that to hit. Parallelism buys time, not results: the counts are
# the serial run's counts, so the suite's own total is measured, never written here.
- run: uv run pytest tests/ -v -n auto --dist loadfile
- uses: actions/setup-node@v5
with:
node-version: "22"
- name: Install Node deps (GUI + renderer)
run: |
# GUI: `--omit=dev` keeps electron/electron-builder out (the suite is
# plain `node --test`), exactly as the pre-existing step did. Renderer
# needs its devDependencies - vitest, tsc and vite all live there.
(cd emrg/gui && npm ci --omit=dev)
(cd emrg/gui/renderer && npm ci)
# #1125 lesson: pytest guards the Agent.md counts *statically* (it counts
# `it(`/`test(` in the source), and the pytest job has no node_modules to
# check that model against the runners. Four separate escapes have been
# found by hand (chained members, line-split calls, tagged templates,
# nested subtests) - each one a form the static regex cannot see while the
# runner executes it. `check-node-test-count.py` closes that loop the other
# way round: it asks vitest and `node --test` what they actually executed
# and fails if Agent.md disagrees. It needs node_modules, which is why it
# lives here rather than in the pytest job.
#
# ⚠️ No flag: the *bare* invocation is the check mode (exit 1 on drift,
# writes nothing). `--dry-run` is the preview of a repair and exits 0 even
# when it prints FAIL - measured 2026-09-11, so gating on it would have
# been a green step that never fails. `--write` is for a human/host.
- name: Agent.md Node counts match the real runners
run: uv run --no-sync python3 scripts/check-node-test-count.py
- name: GUI unit tests (G55)
working-directory: emrg/gui
run: EMRG_SKIP_INTEGRATION=1 npm test
- name: GUI renderer React suite (migration Batch 0)
working-directory: emrg/gui/renderer
run: |
npm run typecheck
npm test
npm run build
- name: GUI integration tests (G73 isolated HOME)
working-directory: emrg/gui
run: npm run test:integration
- name: GUI syntax check (main/preload/renderer)
working-directory: emrg/gui
run: |
node --check main.js && node --check preload.js && node --check daemon_client.js
# renderer JS — vanilla files deleted in Batch 5 cleanup (#1024); React
# source is typechecked by tsc in the "GUI renderer React suite" step above
# v0.2.29 教训:Windows pytest 此前只在 tag 触发的 Build Release 跑
# (test.yml 仅 ubuntu)——FakeGitRun git.EXE 大小写 bug(#723)等 Windows-only
# 测试回归要到发版构建才暴露。每个 PR 都应在 Windows 上跑 pytest 门禁。
test-windows:
runs-on: windows-2025
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.13"
- run: uv sync
# issue #1132 教训(v0.2.94 后):`check-node-test-count.py` 的两个缺陷
# (裸 `npm` 在 Windows 起不来 = WinError 2;locale 解码吞掉 UnicodeDecodeError
# 后 `stdout=None` 抛裸 TypeError)都**只在这个平台可见**,却双双穿过全绿 CI。
# 根因是这里只跑 pytest:那些探针在 `shutil.which("npm")` 为 None 时
# `pytest.skip`(合理——缺工具链不是 `_run` 的缺陷),于是"本平台有没有 Node"
# 变成了**镜像预装的偶然**,而不是这条 job 声明过的前提。
# 显式 setup-node 让前提成立:真 runner 探针在此真正执行(pre-fix 会红),
# 而不是悄悄跳过。ubuntu 的 `test` job 早已这样钉住(见其上 Node deps 步骤)。
- uses: actions/setup-node@v5
with:
node-version: "22"
- name: Python tests (Windows)
run: uv run pytest tests/ -v -n auto --dist loadfile
# v0.2.97 教训(Build Release 35479263507):GUI 单测此前只在 ubuntu 的 `test`
# job 跑过——Windows 上要等 tag 触发的 Build Release 才第一次执行。两条 #1276
# 测试真的拉起了 daemon;POSIX 删得掉活进程 cwd 所在的目录,Windows 删不掉
# (EBUSY)→ 发版构建在 "GUI unit tests" 变红,而每个 PR 的 CI 全绿。命令与
# build-release.yml 的同名步骤保持一致(`--omit=dev`:套件是 `node --test`,
# 不需要 electron/electron-builder),让平台差异在 PR 阶段就暴露,而不是在
# 已经打出去的 tag 上。集成测试仍只在 POSIX 跑(Windows 上 spawn+HOME 语义不同)。
- name: GUI unit tests (Windows)
working-directory: emrg/gui
shell: bash
run: |
npm ci --omit=dev
EMRG_SKIP_INTEGRATION=1 npm test
# v0.2.30 教训(Build Release 31661378619):Test 全绿 ≠ .iss 能编译。
# make-installer.sh 的 emrg.iss 只在 tag 触发 Build Release 时经 iscc 编译,
# #727 误用 LoadStringFromFile 单参数形式(Inno 真实签名是 2 参数 out-param)
# → iscc "Invalid number of parameters" 发版才暴露。此处用 runner 预装的
# iscc 编译渲染出的 emrg.iss(stub payload),PR CI 即拦截 .iss 语法/签名错误。
- name: Inno Setup script compile smoke test
shell: bash
run: |
set -euo pipefail
STAGE="$(mktemp -d)"
mkdir -p "$STAGE/payload/bin" "$STAGE/dist/artifacts"
touch "$STAGE/payload/bin/stop_all.py"
# icon.ico 是 gen-assets 产物(未入库)——生成最小合法 .ico 供 iscc 编译期
# SetupIconFile 检查;{app}(={%USERPROFILE}\.emrg\install)引用的文件
# (UninstallDisplayIcon/[Icons]/[UninstallRun])也需存在。
uv run python - <<'PY'
import struct
def write_ico(path):
hdr = struct.pack('<HHH', 0, 1, 1)
entry = struct.pack('<BBBBHHII', 1, 1, 0, 0, 1, 32, 44, 22)
bih = struct.pack('<IiiHHIIiiII', 40, 1, 1, 1, 32, 0, 4, 0, 0, 0, 0)
open(path, 'wb').write(hdr + entry + bih + b'\x00\x00\x00\x00')
write_ico('packaging/assets/icon.ico')
PY
APP="$USERPROFILE/.emrg/install"
mkdir -p "$APP/bin" "$APP/emrg-gui/EMRG"
touch "$APP/bin/emrg.cmd" "$APP/bin/python.exe" "$APP/emrg-gui/EMRG/EMRG.exe"
# 渲染 make-installer.sh 的 emrg.iss heredoc(变量展开与真实构建一致)。
# 先赋值再统一 export(单行 export 内自引用触发 shellcheck SC2318/SC2097/SC2098)。
VERSION="0.0.0-smoke"
DIST_WIN="$(cygpath -m "$STAGE/dist")"
STAGE_WIN="$(cygpath -m "$STAGE")"
ROOT_WIN="$(cygpath -m "$PWD")"
export VERSION DIST_WIN STAGE_WIN ROOT_WIN STAGE
# 单引号内避免 \$ 字面量(shellcheck SC2016),用无 $ 的锚定模式
sed -n '/cat > .*emrg\.iss.*<</,/^EOF$/p' packaging/make-installer.sh > "$STAGE/gen.sh"
bash "$STAGE/gen.sh"
command -v iscc >/dev/null 2>&1 || { echo "::error::iscc not on PATH (runner image regression)"; exit 1; }
iscc "$STAGE/emrg.iss"
# R131(rant 2026-09-30T11:43:44):元数据必须**真的编译进产物**,所以这条断言
# 读的是 iscc 刚写出的 exe 的版本资源,不是 .iss 的文本 —— 读文本只能证明写了
# 指令。缺任一项即失败:一个字段为空的 exe 就是当初被判
# Trojan:Win32/Sabsik.FL.A!ml 的那种匿名自解压包,也是 SignPath 拒收的形态。
EXE="$STAGE/dist/artifacts/EMRG-$VERSION-windows-x64.exe"
test -f "$EXE" || { echo "::error::iscc did not produce $EXE"; exit 1; }
powershell -NoProfile -Command "
\$v = (Get-Item -LiteralPath '$STAGE_WIN/dist/artifacts/EMRG-$VERSION-windows-x64.exe').VersionInfo
\$missing = @()
if (-not \$v.CompanyName) { \$missing += 'CompanyName' }
if (-not \$v.ProductName) { \$missing += 'ProductName' }
if (-not \$v.FileDescription) { \$missing += 'FileDescription' }
if (-not \$v.FileVersion) { \$missing += 'FileVersion' }
if (\$missing.Count) {
Write-Host \"::error::exe version resource is missing: \$(\$missing -join ', ')\"
exit 1
}
Write-Host \"version resource OK: \$(\$v.CompanyName) / \$(\$v.ProductName) / \$(\$v.FileDescription) / \$(\$v.FileVersion)\"
"