From 0436a155fa26e0fcd238a4b2be26c61c8ffef6ce Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Fri, 23 Oct 2026 17:00:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(doctor):=20=E5=8F=B0=E5=BC=8F=E6=9C=BA?= =?UTF-8?q?=E4=B8=8E=E8=99=9A=E6=8B=9F=E6=9C=BA=E4=B8=8A=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=20sleep-policy=20=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现要求 `pmset -g custom` 同时提供 AC 与电池两个分支,否则整项跳过; Mac mini/Studio/iMac 与 CI 虚拟机只有 `AC Power:` 分支,于是这项诊断从未生效 (GitHub macOS 虚拟机上 CI 每次红灯:`C7-doctor-sleep-policy-on-macos`)。 - power.go:SleepPolicies 返回「实际存在的分支」(AC 在前),新增纯函数 ParseSleepPolicies 便于单测;ParseSleepPolicy 语义不变 - commands.go:按分支逐一诊断并回显,任一分支有矛盾即 ok=false - 单测:AC-only(台式机)、双分支顺序、无分支三种情形 - conformance:注入假 pmset 固定「双分支 / 仅 AC / 矛盾组合」三组用例, 不再断言宿主真实电源状态 验证:make quick(73 用例全绿);`ew doctor` 在 AC-only 假 pmset 下输出 「sleep-policy AC sleep=0m displaysleep=30m disksleep=0m(自洽)」 --- CHANGELOG.md | 9 +++- docs/spec/08-keepawake.md | 7 ++- internal/cli/commands.go | 30 +++++++----- internal/platform/platform_test.go | 47 +++++++++++++++++++ internal/platform/power.go | 25 +++++++--- tests/conformance/c4_cli.bats | 74 +++++++++++++++++++++++++++--- 6 files changed, 165 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1703908..abb6be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ ## [Unreleased] -(暂无) +### 修复 + +- **`ew doctor` 在台式机 / 虚拟机上丢失 `sleep-policy` 检查**:原实现要求 `pmset -g custom` + 同时提供 AC 与电池两个分支,否则整项跳过;Mac mini/Studio/iMac 与 CI 虚拟机只有 + `AC Power:` 分支,因此这项诊断从未生效(GitHub macOS 虚拟机上的 CI 一直因此红灯)。 + 现在按分支是否存在分别诊断并回显(`AC` 在前、`Battery` 在后),`sleep=0` 仍不判为矛盾 +- **conformance 的 `sleep-policy` 用例改为注入假 `pmset`**:原先直接断言宿主 `pmset` 输出, + 随 CI 机型漂移而失败;现覆盖「两侧」「仅 AC(台式机)」「矛盾组合」三组确定性用例 ## [0.4.4] - 2026-10-23 diff --git a/docs/spec/08-keepawake.md b/docs/spec/08-keepawake.md index 038e005..fc31a8a 100644 --- a/docs/spec/08-keepawake.md +++ b/docs/spec/08-keepawake.md @@ -116,7 +116,12 @@ persist = "false" # 是否写入开机自启单元 | `displaysleep >= sleep` | displaysleep 应小于 sleep(否则熄屏设置不生效) | | `disksleep == 0` | disksleep 应为非零(sleep 非零时) | -- **AC 与电池两侧都检查**并同时回显(只看当前分支会漏掉另一侧),`sleep=0`(永不)不判为矛盾; +- **存在的分支都检查**并回显(`AC` 在前、`Battery` 在后,`;` 分隔):只看当前分支 + 会漏掉另一侧的矛盾;台式机与虚拟机只有 `AC`,此时照常输出只含 `AC` 的该项 + (**不得**因缺电池分支而整体跳过——这是 0.4.4 的缺陷,已由 C7 用例锁死); +- `sleep=0`(永不)不判为矛盾; +- `detail` 形如 `AC sleep=60m displaysleep=30m disksleep=10m;Battery sleep=30m displaysleep=10m disksleep=10m(自洽)`, + 有矛盾时 `ok=false` 且把问题附在括号内; - 非 macOS 平台不输出该项;`--json` 中同样只有 macOS 出现 `sleep-policy`。 ## 7. 运行态与幂等 diff --git a/internal/cli/commands.go b/internal/cli/commands.go index 1a6d265..f881f50 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -253,21 +253,27 @@ func (a *App) doctorChecks() []checkItem { checks = append(checks, checkItem{Name: "console", OK: cp == 0 || cp == 65001, Detail: detail}) // 电源策略自洽性(仅 macOS):pmset 自身会对矛盾组合告警,但容易只读不看。 - // AC 与电池两侧都报:只报当前分支会漏掉另一侧的问题。 - if ac, bat, ok := platform.SleepPolicies(); ok { - var problems []string - for _, br := range []struct { - name string - pol platform.SleepPolicy - }{{"AC", ac}, {"Battery", bat}} { - for _, p := range br.pol.Problems() { - problems = append(problems, br.name+": "+p) + // 存在的分支都要报:只报当前分支会漏掉另一侧的问题;台式机/虚拟机只有 AC, + // 此时照常诊断(不能因为缺电池分支就整体跳过)。 + if pols := platform.SleepPolicies(); len(pols) > 0 { + var ( + problems []string + parts []string + ) + for _, pol := range pols { + name := "Battery" + if pol.OnAC { + name = "AC" + } + parts = append(parts, fmt.Sprintf("%s sleep=%dm displaysleep=%dm disksleep=%dm", + name, pol.Sleep, pol.DisplaySleep, pol.DiskSleep)) + for _, p := range pol.Problems() { + problems = append(problems, name+": "+p) } } - detail := fmt.Sprintf("AC sleep=%dm displaysleep=%dm disksleep=%dm;Battery sleep=%dm displaysleep=%dm disksleep=%dm", - ac.Sleep, ac.DisplaySleep, ac.DiskSleep, bat.Sleep, bat.DisplaySleep, bat.DiskSleep) + detail := strings.Join(parts, ";") if len(problems) == 0 { - detail += "(两侧自洽)" + detail += "(自洽)" } else { detail += "(" + strings.Join(problems, ";") + ")" } diff --git a/internal/platform/platform_test.go b/internal/platform/platform_test.go index 64a4a70..41acbef 100644 --- a/internal/platform/platform_test.go +++ b/internal/platform/platform_test.go @@ -149,3 +149,50 @@ AC Power: t.Fatalf("两侧自洽却报问题: ac=%v bat=%v", ac.Problems(), bat.Problems()) } } + +// 台式机 / CI 虚拟机只有 AC 分支:必须照常返回 AC,不得因缺电池而整体放弃。 +func TestParseSleepPoliciesDesktopACOnly(t *testing.T) { + out := `AC Power: + displaysleep 30 + sleep 0 + disksleep 0 +` + pols := ParseSleepPolicies(out) + if len(pols) != 1 { + t.Fatalf("台式机应只解析出 AC 分支,得到 %d 个: %+v", len(pols), pols) + } + if !pols[0].OnAC || pols[0].DisplaySleep != 30 || pols[0].Sleep != 0 { + t.Fatalf("AC 分支解析错误: %+v", pols[0]) + } + // sleep=0(永不)不判为矛盾。 + if len(pols[0].Problems()) != 0 { + t.Fatalf("sleep=0 应视为自洽,得到 %v", pols[0].Problems()) + } +} + +// AC 在前、电池在后的稳定顺序(doctor 的 detail 依赖它)。 +func TestParseSleepPoliciesOrderAndCount(t *testing.T) { + out := `Battery Power: + sleep 30 + displaysleep 10 + disksleep 10 +AC Power: + sleep 60 + displaysleep 30 + disksleep 10 +` + pols := ParseSleepPolicies(out) + if len(pols) != 2 { + t.Fatalf("两侧齐全时应返回 2 个分支,得到 %d 个: %+v", len(pols), pols) + } + if !pols[0].OnAC || pols[1].OnAC { + t.Fatalf("分支顺序应为 AC、Battery,得到 %+v", pols) + } +} + +// 无任何可解析分支(pmset 输出异常)时返回空,doctor 不输出该项。 +func TestParseSleepPoliciesEmpty(t *testing.T) { + if pols := ParseSleepPolicies("garbage\n"); len(pols) != 0 { + t.Fatalf("无分支时应返回空,得到 %+v", pols) + } +} diff --git a/internal/platform/power.go b/internal/platform/power.go index ff6c82f..9c4cee0 100644 --- a/internal/platform/power.go +++ b/internal/platform/power.go @@ -45,19 +45,30 @@ type SleepPolicy struct { OnAC bool } -// SleepPolicies 一次性读取 AC 与电池两侧的 pmset 策略。 +// SleepPolicies 一次性读取 pmset 的电源策略分支。 // 两侧都要看:只报当前分支会漏掉另一侧的矛盾(拔电后才发现设置没生效)。 -func SleepPolicies() (ac, battery SleepPolicy, ok bool) { +// 返回的切片只含**实际存在**的分支(AC 在前):台式机与虚拟机没有电池分支, +// 若强行要求两侧齐全,这些机器会永远拿不到 sleep-policy 诊断。 +func SleepPolicies() []SleepPolicy { if DetectOS() != MacOS { - return SleepPolicy{}, SleepPolicy{}, false + return nil } out, err := exec.Command("pmset", "-g", "custom").Output() if err != nil { - return SleepPolicy{}, SleepPolicy{}, false + return nil } - ac, okAC := ParseSleepPolicy(string(out), true) - battery, okBat := ParseSleepPolicy(string(out), false) - return ac, battery, okAC && okBat + return ParseSleepPolicies(string(out)) +} + +// ParseSleepPolicies 从 `pmset -g custom` 输出中解析出实际出现的分支(AC 在前)。 +func ParseSleepPolicies(text string) []SleepPolicy { + var pols []SleepPolicy + for _, onAC := range []bool{true, false} { + if pol, ok := ParseSleepPolicy(text, onAC); ok { + pols = append(pols, pol) + } + } + return pols } // ParseSleepPolicy 从 `pmset -g custom` 输出中解析指定电源分支。 diff --git a/tests/conformance/c4_cli.bats b/tests/conformance/c4_cli.bats index 6601f94..8ceec98 100644 --- a/tests/conformance/c4_cli.bats +++ b/tests/conformance/c4_cli.bats @@ -86,14 +86,76 @@ load helpers [ ! -e "$HOME/.vimrc" ] } -# doctor 的电源策略自洽性检查(仅 macOS;pmset 矛盾组合容易"设了但没生效") -@test "C7-doctor-sleep-policy-on-macos" { +# doctor 的电源策略自洽性检查(仅 macOS)。 +# 用假 pmset 固定输出,避免 VM/台式机 pmset 内容差异导致误报(曾经因此让 CI 红灯)。 +# 假 pmset 只提供数据,不改宿主电源设置。 +fake_pmset() { + mkdir -p "$TEST_HOME/bin" + cat > "$TEST_HOME/bin/pmset" + chmod +x "$TEST_HOME/bin/pmset" + printf '%s' "$TEST_HOME/bin" +} + +@test "C7-doctor-sleep-policy-macos-two-branches" { [ "$(uname -s)" = "Darwin" ] || skip "macOS only" - ew_capture doctor --json --offline + bin="$(fake_pmset << 'SH' +#!/bin/sh +cat << 'OUT' +Battery Power: + sleep 30 + displaysleep 10 + disksleep 10 +AC Power: + sleep 60 + displaysleep 30 + disksleep 10 +OUT +SH +)" + PATH="$bin:$PATH" ew_capture doctor --json --offline + [ "$EW_RC" -eq 0 ] + # 两侧都回显(AC 在前),顺序固定 + [[ "$EW_OUT" == *'"name":"sleep-policy","ok":true'* ]] + [[ "$EW_OUT" == *'AC sleep=60m displaysleep=30m disksleep=10m;Battery sleep=30m displaysleep=10m disksleep=10m'* ]] +} + +# 台式机与 CI 虚拟机只有 AC 分支:必须照常输出该项(缺电池不得整体跳过) +@test "C7-doctor-sleep-policy-macos-desktop-ac-only" { + [ "$(uname -s)" = "Darwin" ] || skip "macOS only" + bin="$(fake_pmset << 'SH' +#!/bin/sh +cat << 'OUT' +AC Power: + sleep 0 + displaysleep 30 + disksleep 0 +OUT +SH +)" + PATH="$bin:$PATH" ew_capture doctor --json --offline + [ "$EW_RC" -eq 0 ] + [[ "$EW_OUT" == *'"name":"sleep-policy","ok":true'* ]] + [[ "$EW_OUT" == *'AC sleep=0m displaysleep=30m disksleep=0m'* ]] + [[ "$EW_OUT" != *'Battery sleep='* ]] +} + +@test "C7-doctor-sleep-policy-macos-flags-conflicts" { + [ "$(uname -s)" = "Darwin" ] || skip "macOS only" + bin="$(fake_pmset << 'SH' +#!/bin/sh +cat << 'OUT' +AC Power: + sleep 30 + displaysleep 30 + disksleep 0 +OUT +SH +)" + PATH="$bin:$PATH" ew_capture doctor --json --offline [ "$EW_RC" -eq 0 ] - [[ "$EW_OUT" == *'"name":"sleep-policy"'* ]] - [[ "$EW_OUT" == *'displaysleep='* ]] - [[ "$EW_OUT" == *'disksleep='* ]] + [[ "$EW_OUT" == *'"name":"sleep-policy","ok":false'* ]] + [[ "$EW_OUT" == *'displaysleep 应小于 sleep'* ]] + [[ "$EW_OUT" == *'disksleep 应为非零'* ]] } @test "C7-doctor-no-sleep-policy-off-macos" { From 461d77835cda01c88c9c28cdd8803af5cb65aa7a Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Fri, 23 Oct 2026 17:00:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=E5=88=86=E5=B1=82=E9=97=A8=E7=A6=81?= =?UTF-8?q?=20+=20=E5=8D=95=E6=AC=A1=E5=BF=AB=E9=80=9F=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=EF=BC=88quick=20=E5=BF=AB=E8=BD=A6=E9=81=93=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E7=9F=A9=E9=98=B5=E6=94=B6=E6=95=9B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 CI 拆成快车道与重矩阵,让一次迭代的反馈稳定在 2 分钟内: - 新增 `Changed paths`:判定是否只有 docs/site/*.md 改动,纯文档提交不触发重矩阵 - 新增 `Quick gate`(Ubuntu,~1–2 分钟):ShellCheck + bash -n + shfmt + gofmt + go vet + go test + conformance;每次 push/PR 都跑 - 重矩阵(macOS ShellCheck、macOS/Windows conformance + Windows 原生冒烟、 三平台安装链路 e2e、6 目标交叉编译)改为:仅 main 推送、确有代码改动、 且 quick 通过后运行(`needs: quick`,红灯不再浪费平台机时) - PR 只跑快车道;`workflow_dispatch` 支持 `scope=fast|full`; 同分支新推送取消旧运行(concurrency.cancel-in-progress) 配套: - `make quick`:本地 < 1 分钟门禁(shfmt 检查 + go test + conformance) - `docs/ITERATION.md`:分层门禁、单次快速迭代、提交/分支/发布规范, 并写明「依赖宿主环境的诊断项必须注入假命令」这条踩坑结论 - CONTRIBUTING vs AGENTS vs ACCEPTANCE 同步(CI 任务映射、用例数 71 → 73) 验证:make quick 全绿(73 用例,18s);本地 ShellCheck + shfmt 无差异; ci.yml 通过 YAML 解析校验 --- .github/workflows/ci.yml | 165 +++++++++++++++++++++++++++++++-------- AGENTS.md | 14 ++-- CHANGELOG.md | 12 +++ CONTRIBUTING.md | 18 +++-- Makefile | 5 +- README.md | 2 +- docs/ACCEPTANCE.md | 26 +++--- docs/ITERATION.md | 72 +++++++++++++++++ 8 files changed, 261 insertions(+), 53 deletions(-) create mode 100644 docs/ITERATION.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7af5432..2a65b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,50 +1,93 @@ name: CI +# 迭代分层(详见 docs/ITERATION.md): +# - pull_request → 只跑 quick(~1 分钟):PR 反馈快,重矩阵留给 main +# - push:main → quick + 重矩阵(仅当有代码改动时;纯文档/站点提交自动跳过) +# - workflow_dispatch → 手动选 fast / full,用于「只跑一次快速迭代」 on: push: branches: [main] pull_request: branches: [main] + workflow_dispatch: + inputs: + scope: + description: 'fast = 只跑快速门禁(约 1 分钟);full = 全量平台矩阵' + type: choice + options: [fast, full] + default: fast -jobs: - shell-lint: - name: Shell lint (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] +permissions: + contents: read + +# 单次迭代只占一条流水线:同分支新提交立刻取消仍在跑的旧运行 +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: '1.24' +jobs: + changes: + name: Changed paths + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + code: ${{ steps.filter.outputs.code }} steps: - name: Checkout uses: actions/checkout@v5 - - - name: Cache Tools - uses: actions/cache@v5 with: - path: | - /usr/local/bin/shellcheck - /usr/local/bin/shfmt - ~/go/bin/shfmt - key: ${{ runner.os }}-easywork-shell-tools-${{ hashFiles('.github/workflows/ci.yml') }} + fetch-depth: 0 - - name: Install ShellCheck + - name: Detect code changes + id: filter shell: bash run: | - if command -v brew >/dev/null 2>&1; then - brew install shellcheck + set -euo pipefail + base="" + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + base="${{ github.event.pull_request.base.sha }}" + else + base="${{ github.event.before }}" + fi + # 基线不可用(首次推送、force push、手动触发)→ 按"有代码改动"处理 + if [[ -z "$base" || "$base" =~ ^0+$ ]] || ! git cat-file -e "${base}^{commit}" 2> /dev/null; then + echo "基线不可用,按有代码改动处理" + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + changed="$(git diff --name-only "$base" HEAD)" + printf '改动的文件:\n%s\n' "$changed" + # 纯 docs/site/markdown 提交不触发重矩阵 + if printf '%s\n' "$changed" | grep -Eqv '^(docs/|site/|.*\.md$|LICENSE$)'; then + echo "code=true" >> "$GITHUB_OUTPUT" else - sudo apt-get update -y && sudo apt-get install -y shellcheck + echo "code=false" >> "$GITHUB_OUTPUT" fi - - name: Install shfmt + # ── 快车道:每次迭代必过,目标 < 2 分钟 ───────────────────── + quick: + name: Quick gate (${{ github.event_name }}) + runs-on: ubuntu-latest + needs: changes + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + + - name: Install shell tools shell: bash run: | - if command -v brew >/dev/null 2>&1; then - brew install shfmt - else - sudo apt-get install -y shfmt || go install mvdan.cc/sh/v3/cmd/shfmt@v3.10.0 - fi + sudo apt-get update -y + sudo apt-get install -y shellcheck shfmt # Bash 引擎已于 0.4.0 退役;仅引导/辅助脚本保留 shell 检查。 - name: ShellCheck(bootstrap + scripts) @@ -63,9 +106,60 @@ jobs: shell: bash run: shfmt -d -i 4 -bn -ci -sr bootstrap.sh scripts + - name: gofmt + go vet + shell: bash + run: | + set -euo pipefail + unformatted="$(gofmt -l ./cmd ./internal)" + if [[ -n "$unformatted" ]]; then + echo "需要 gofmt:" + echo "$unformatted" + exit 1 + fi + go vet ./... + + - name: Go unit tests + shell: bash + run: go test ./... + + - name: Install BATS + shell: bash + run: | + git clone --depth 1 https://github.com/bats-core/bats-core.git "$RUNNER_TEMP/bats" + sudo "$RUNNER_TEMP/bats/install.sh" /usr/local + + - name: Conformance (Go) + shell: bash + run: tests/conformance/run.sh go + + # ── 重矩阵:仅 main 推送 / 手动 full,且确有代码改动 ──────── + shell-lint: + name: Shell lint (macos-latest) + runs-on: macos-latest + needs: [changes, quick] + if: needs.changes.outputs.code == 'true' && github.event_name != 'pull_request' && inputs.scope != 'fast' + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install shell tools + shell: bash + run: brew install shellcheck shfmt + + - name: ShellCheck + shfmt(macOS 自带 bash 3.2 语义) + shell: bash + run: | + set -euo pipefail + shellcheck bootstrap.sh scripts/*.sh + shfmt -d -i 4 -bn -ci -sr bootstrap.sh scripts + install-e2e: name: Install chain e2e (${{ matrix.os }}) runs-on: ${{ matrix.os }} + needs: [changes, quick] + if: needs.changes.outputs.code == 'true' && github.event_name != 'pull_request' && inputs.scope != 'fast' + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -78,7 +172,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: ${{ env.GO_VERSION }} cache: false # 安装入口必须有自动化门禁;所有链路只访问 127.0.0.1 的本地镜像,不触网。 @@ -110,10 +204,14 @@ jobs: go-engine: name: Go engine (${{ matrix.os }}) runs-on: ${{ matrix.os }} + needs: [changes, quick] + if: needs.changes.outputs.code == 'true' && github.event_name != 'pull_request' && inputs.scope != 'fast' + timeout-minutes: 20 strategy: fail-fast: false + # ubuntu 已由 quick 覆盖(单测 + conformance),这里只补平台差异 matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [macos-latest, windows-latest] steps: - name: Checkout @@ -122,7 +220,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: ${{ env.GO_VERSION }} cache: false - name: Go unit tests @@ -148,7 +246,7 @@ jobs: if: runner.os != 'Windows' shell: bash run: | - if command -v brew >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1; then brew install bats-core else git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats @@ -187,11 +285,14 @@ jobs: "$RUNNER_TEMP/ew.exe" config set git.identities.personal.name 'Win User' "$RUNNER_TEMP/ew.exe" config get git.identities.personal.name | grep -q 'Win User' "$RUNNER_TEMP/ew.exe" install shell git --yes --offline - grep -q 'managed section' "$RUNNER_TEMP/home/.sh_config_custom" 2>/dev/null || grep -rq 'managed section' "$RUNNER_TEMP/home/Documents" 2>/dev/null + grep -q 'managed section' "$RUNNER_TEMP/home/.sh_config_custom" 2> /dev/null || grep -rq 'managed section' "$RUNNER_TEMP/home/Documents" 2> /dev/null cross-compile: name: Cross-compile (6 targets) runs-on: ubuntu-latest + needs: [changes, quick] + if: needs.changes.outputs.code == 'true' && github.event_name != 'pull_request' && inputs.scope != 'fast' + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v5 @@ -199,7 +300,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: ${{ env.GO_VERSION }} cache: false - name: Build darwin/linux/windows x amd64/arm64 diff --git a/AGENTS.md b/AGENTS.md index 2d7c052..d2923dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,8 @@ ## 3. 常用命令 ```bash -make check # lint + fmt-check + gotest + conformance + 安装链路(提交前必跑) +make quick # 快速门禁(< 1 分钟:shfmt 检查 + go test + conformance;提交前跑这个) +make check # 完整质量门禁(lint + fmt-check + gotest + conformance + 安装链路) make gotest # go test ./... make test # 黑盒 conformance(tests/conformance,仅 Go) make lint # ShellCheck(bootstrap.sh + scripts/) @@ -77,6 +78,7 @@ bootstrap.sh|.ps1 # 下载并安装二进制(唯一安装入口) | `README.md` | 用户入口:安装、命令、模块、配置、防休眠 | | `docs/spec/` | **规范性契约**(行为等价判据),改行为先改这里 | | `docs/ACCEPTANCE.md` | 验收项与 CI 对应关系 | +| `docs/ITERATION.md` | **迭代流程**:分层门禁、单次快速迭代、提交/发布规范 | | `docs/RETIREMENT.md` / `docs/ROLLBACK.md` | 退役记录与回滚方式 | | `DESIGN.md` | 设计决策与迁移历史(**非规范**,冲突以 `docs/spec/` 为准) | | `site/` | 产品页(GitHub Pages,`https://easywork.jokerhub.cn/`) | @@ -101,8 +103,10 @@ bootstrap.sh|.ps1 # 下载并安装二进制(唯一安装入口) 3. **新增模块**:`internal/modules/xxx.go` → 加入 `registry`(字母序)→ 补 `*_test.go` → 视需要补 `tests/conformance/*.bats`。 4. **目标文件写入**只允许经 engine 的声明式函数,禁止模块手写备份/标记逻辑。 -5. 新增功能必须补测试;改动后先跑 `make check`。 +5. 新增功能必须补测试;改动后先跑 `make quick`,提交前如需完整验证再跑 `make check`。 6. **不要恢复 Bash 引擎**;`make retire-check` 会拦。 +7. **依赖宿主环境的诊断项**(`pmset`/`powercfg`/电源/时间等)的 conformance 用例必须 + 注入假命令固定输出,不得断言宿主真实状态(否则会像 GitHub macOS 虚拟机那样长期红灯)。 ## 9. 测试约定 @@ -115,9 +119,9 @@ bootstrap.sh|.ps1 # 下载并安装二进制(唯一安装入口) 解析无 BOM 脚本(脚本内有自检,CI 也有断言) - Go 单测:纯函数与可注入依赖(`Runner`/`Clock` 等),不得改动宿主真实状态 (如 keepawake 测试注入 fake runner,不真的起 caffeinate/launchctl) -- CI:`shell-lint`(shellcheck+shfmt)、`install-e2e`(三平台:Unix 跑 `bootstrap-e2e.sh`, - Windows 跑 `bootstrap-e2e.ps1`)、`go-engine`(三平台 go test + conformance + Windows smoke)、 - `cross-compile`(6 目标) +- CI:分两层——`quick`(每次 push/PR:shell 检查 + gofmt/vet + go test + conformance, + ~1–2 分钟)与重矩阵(仅 main 推送且确有代码改动:macOS ShellCheck、macOS/Windows + conformance、三平台安装链路、6 目标交叉编译)。PR 只跑快车道,流程见 `docs/ITERATION.md` - Windows 安装链路在 CI 上跑**两个宿主**:Windows PowerShell 5.1 与 PowerShell 7 (`-ChildShell powershell|pwsh`),并额外覆盖文档里的 `irm | iex` 用法 - `scripts/bootstrap-e2e.ps1` 必须保持**纯 ASCII**:Windows PowerShell 5.1 会按 ANSI diff --git a/CHANGELOG.md b/CHANGELOG.md index abb6be5..7780c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ - **conformance 的 `sleep-policy` 用例改为注入假 `pmset`**:原先直接断言宿主 `pmset` 输出, 随 CI 机型漂移而失败;现覆盖「两侧」「仅 AC(台式机)」「矛盾组合」三组确定性用例 +### 变更 + +- **CI 分层 + 单次快速迭代**:新增 `Changed paths` 判定与 `Quick gate`(Ubuntu,~1–2 分钟, + 含 ShellCheck / `bash -n` / shfmt / `gofmt` / `go vet` / `go test` / conformance); + PR 只跑快车道,重矩阵(macOS ShellCheck、macOS/Windows conformance + Windows 原生冒烟、 + 三平台安装链路 e2e、6 目标交叉编译)仅在 `main` 推送且确有代码改动时运行, + 纯 docs/site/markdown 提交自动跳过;`workflow_dispatch` 支持 `scope=fast|full`; + 同分支新推送取消旧运行;重矩阵 `needs: quick`,红灯不再浪费平台机时 +- 新增 `make quick`(本地 < 1 分钟门禁)与 `docs/ITERATION.md`(分层门禁、单次快速迭代、 + 提交/分支/发布规范);`CONTRIBUTING.md`、`AGENTS.md`、`docs/ACCEPTANCE.md` 同步 +- conformance 用例数 71 → 73 + ## [0.4.4] - 2026-10-23 ### 新增 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10644e7..ebd1ccc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,15 +11,22 @@ ## 提 Pull Request 1. Fork 本仓库 -2. 创建特性分支: +2. 创建短生命周期分支(`feat/…`、`fix/…`、`docs/…`、`ci/…`): ```bash - git checkout -b feature/my-feature + git switch -c fix/my-fix ``` -3. 开发并确保质量通过: +3. 开发并确保快速门禁通过(< 1 分钟): ```bash - make check # ShellCheck + shfmt + go test + conformance + 安装链路 + make quick # shfmt 检查 + go test + conformance ``` -4. 提交 PR 到 `main` 分支 + 需要完整验证时跑 `make check`(外加 ShellCheck 与安装链路 e2e)。 +4. 提交到特性分支并创建 PR 到 `main`: + ```bash + git commit -m 'fix(doctor): 一句话说明改了什么、为什么' + git push -u origin HEAD && gh pr create --fill + ``` +5. PR 上 CI 只跑**快车道**(约 2 分钟);合并到 `main` 后自动跑 macOS/Windows 矩阵 + 与安装链路。提交规范与发布流程见 [docs/ITERATION.md](docs/ITERATION.md)。 ## 开发环境 @@ -49,6 +56,7 @@ sudo /tmp/bats/install.sh /usr/local | `make test` | 黑盒 conformance(`tests/conformance`,只测 Go) | | `make lint` / `make fmt` / `make fmt-check` | shell 脚本检查与格式化 | | `make check` | 完整质量检查(含安装链路 e2e) | +| `make quick` | 快速门禁(< 1 分钟,提交前用) | | `make install-e2e` | bootstrap + update 本地镜像端到端(不触网) | | `make acceptance` | 一键验收(含交叉编译、退役不变量) | | `make retire-check` | 断言 Bash 引擎没有复活 | diff --git a/Makefile b/Makefile index 98e8899..7967a76 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: lint test fmt fmt-check check check-all install clean help dry-run conform build gotest acceptance update-e2e bootstrap-e2e install-e2e retire-check site +.PHONY: lint test fmt fmt-check check check-all quick install clean help dry-run conform build gotest acceptance update-e2e bootstrap-e2e install-e2e retire-check site # ── EasyWork Makefile ────────────────────────────────────────── # @@ -52,6 +52,9 @@ endif check: lint fmt-check gotest test install-e2e ## Run all checks (shell + Go + conformance + 安装链路) @echo "✅ All checks passed" +quick: fmt-check gotest test ## 快速门禁(< 1 分钟:shell 格式 + 单测 + conformance;提交前必跑) + @echo "✅ Quick gate passed(发 PR 前跑 quick 即可;重矩阵由 CI 在 main 上跑)" + check-all: check ## 兼容旧习惯:check-all 等同 check build: ## Build the Go binary into .build/ew diff --git a/README.md b/README.md index 5d7b5d3..d329aad 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Windows 上 `--json` 恒为 UTF-8;人类可读输出在被管道/重定向时 ```bash make check # ShellCheck + shfmt + go test + conformance + 安装链路 -make test # 黑盒 conformance(tests/conformance,71 用例) +make test # 黑盒 conformance(tests/conformance,73 用例) make install # 安装到 PATH make site # 本地预览产品页 http://127.0.0.1:8765/ make acceptance # 全量验收(含交叉编译与退役不变量) diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index b807741..45c256b 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -15,7 +15,7 @@ | ShellCheck | `bootstrap.sh` + `scripts/*.sh` 零告警 | | shfmt | `shfmt -d -i 4 -bn -ci -sr .` 无差异 | | Go vet + 单测 | `go vet ./...`、`go test ./...` 全绿 | -| Conformance | `tests/conformance/run.sh go` 全绿(71 用例) | +| Conformance | `tests/conformance/run.sh go` 全绿(73 用例) | | 交叉编译 | 6 目标 `{darwin,linux,windows}×{amd64,arm64}` 构建成功 | | 端到端冒烟 | 沙箱内 `status --json` → `keepawake status --json` → `install git shell` → 写入 managed section → `uninstall` 后标记消失 | | 退役不变量 | `make retire-check`:无 Bash 残留、`bootstrap.sh` 带 sha256 校验、发布链路只针对 Go | @@ -39,14 +39,22 @@ make acceptance ## CI 对应关系 -| 任务 | 覆盖 | -|---|---| -| `shell-lint` | ShellCheck + `bash -n` + shfmt(`bootstrap.sh` + `scripts/`,Ubuntu/macOS) | -| `install-e2e` | Ubuntu / macOS / Windows:`bootstrap-e2e.sh` 或 `bootstrap-e2e.ps1`(Windows 用 PS 5.1 与 7 各一次)+ `update-e2e` | -| `go-engine` | 三平台 `go test` + `gofmt`/`go vet` + Go conformance + Windows 原生冒烟 | -| `cross-compile` | 6 目标交叉编译 | -| `Pages` | 产品页:`site/` 直接部署到 GitHub Pages(自定义域名 `easywork.jokerhub.cn`) | -| `release` / `binaries` / `checksums` | 6 目标二进制 + `checksums.txt` + build provenance attestation | +每次迭代的反馈速度由任务分层决定(详见 [ITERATION.md](ITERATION.md)): + +| 任务 | 何时跑 | 覆盖 | +|---|---|---| +| `Changed paths` | 每次 push / PR | 判定是否只有 docs/site/markdown 改动(纯文档跳过重矩阵) | +| `Quick gate` | 每次 push / PR(~1–2 分钟) | Ubuntu:ShellCheck + `bash -n` + shfmt + `gofmt` + `go vet` + `go test` + Go conformance | +| `Shell lint (macos-latest)` | main 推送 + 有代码改动 | macOS 自带 bash 3.2 语义下 ShellCheck + shfmt | +| `Install chain e2e` | main 推送 + 有代码改动 | Ubuntu / macOS / Windows:`bootstrap-e2e.sh` 或 `bootstrap-e2e.ps1`(Windows 用 PS 5.1 与 7 各一次)+ `update-e2e` | +| `Go engine (macos/windows)` | main 推送 + 有代码改动 | 平台差异:`go test` + `gofmt`/`go vet` + Go conformance + Windows 原生冒烟 | +| `Cross-compile` | main 推送 + 有代码改动 | 6 目标交叉编译 | +| `Pages` | `site/**` 改动 | 产品页:`site/` 直接部署到 GitHub Pages(自定义域名 `easywork.jokerhub.cn`) | +| `release` / `binaries` / `checksums` | 打 tag `x.y.z` | 6 目标二进制 + `checksums.txt` + build provenance attestation | + +- PR 上只跑 `changed paths` + `quick`:合入前反馈 ≤ 2 分钟; +- 需要只跑一次快车道:`gh workflow run ci.yml -f scope=fast`; +- 同一分支的新推送会取消旧运行(`concurrency.cancel-in-progress`)。 ## 迁移期历史(已结束) diff --git a/docs/ITERATION.md b/docs/ITERATION.md new file mode 100644 index 0000000..dcddf35 --- /dev/null +++ b/docs/ITERATION.md @@ -0,0 +1,72 @@ +# 迭代流程(单次快速迭代) + +> 目标:**改一行代码到拿到 CI 结论 ≤ 2 分钟**;重矩阵只在必要的时候跑一次。 +> 规范契约见 `docs/spec/`,验收映射见 `docs/ACCEPTANCE.md`。 + +## 1. 分层门禁 + +| 层 | 触发 | 内容 | 耗时 | +|---|---|---|---| +| 本地 `make quick` | 手工 | shfmt 检查 + `go test` + conformance | < 1 分钟 | +| `quick` 任务 | 每次 push / PR | 同上(+ ShellCheck / `bash -n` / `gofmt` / `go vet`) | ~1–2 分钟 | +| 重矩阵 | **仅 main 推送**、确有代码改动、`quick` 已过 | macOS ShellCheck、macOS/Windows conformance + 原生冒烟、三平台安装链路 e2e、6 目标交叉编译 | 10–20 分钟 | +| `Pages` | 仅 `site/**` 改动 | 静态页直接部署 | ~20 秒 | +| `Release` | 打 tag `x.y.z` | 6 目标二进制 + `checksums.txt` + provenance | ~2 分钟 | + +关键设计: + +- **PR 只跑快车道**:PR 上不跑 macOS/Windows 矩阵,反馈 ≤ 2 分钟;全量在合并到 `main` 后跑。 +- **纯文档/站点改动自动降级**:`docs/**`、`site/**`、`*.md`、`LICENSE` 之外没有改动时, + 重矩阵整体跳过(`Changed paths` 任务判定)。 +- **同分支并发取消**:`concurrency.cancel-in-progress`,新提交立刻顶掉还在跑的旧运行, + 不会排队等 15 分钟。 +- **失败即止**:重矩阵 `needs: quick`,快车道红灯时不会浪费 Windows/macOS 机时。 + +## 2. 单次快速迭代(日常) + +```bash +git switch -c fix/doctor-sleep-policy # 分支:feat/ fix/ docs/ ci/ chore/ +# …改代码… +make quick # < 1 分钟,红了先修好再推 +git commit -m 'fix(doctor): …' # 见 §3 提交规范 +git push -u origin HEAD # 或直接 gh pr create +gh pr create --fill # PR 自动跑 quick(~2 分钟) +gh pr checks --watch # 绿灯即可合并 +``` + +合并到 `main` 后自动跑重矩阵;若只改文档/站点,重矩阵会被跳过。 + +**只想跑一次快车道**(不验证平台矩阵): + +```bash +gh workflow run ci.yml -f scope=fast && gh run watch +``` + +## 3. 提交与分支规范 + +提交信息:`type(scope): 中文摘要`,type 取 `feat|fix|docs|refactor|test|chore|ci|perf|polish`。 + +- 摘要一句话说清「改了什么、为什么」,不加句号; +- 行为变化必须在正文写**验证方式**(跑了哪个命令 / 哪个用例); +- 一个提交只做一件事;格式化与逻辑修改分开提交,方便 review 与回滚; +- 分支短生命周期,合并后删除。 + +## 4. 改动落到哪一层 + +| 改动 | 必做 | +|---|---| +| 行为/契约(CLI、JSON、模块、keepawake…) | 先改 `docs/spec/`,再改实现,补 `*_test.go` **和** `tests/conformance/*.bats` | +| 新增模块 | `internal/modules/xxx.go` → `registry`(字母序)→ 单测 → conformance | +| 引导/安装脚本 | ShellCheck + `shfmt -d` + `make install-e2e` | +| 环境相关诊断(`pmset`、`powercfg`…) | conformance 必须注入假命令固定输出,**不得**依赖宿主真实状态 | +| 仅文档/站点 | `make quick` 足以;`site/**` 会自动部署 Pages | + +> 「环境相关诊断」这条是踩过坑的:`sleep-policy` 用例原先直接读宿主 `pmset`, +> GitHub macOS 虚拟机只有 AC 分支 → CI 长期红灯(0.4.4 已修,见 CHANGELOG)。 + +## 5. 发布 + +1. `internal/version/version.go` 改 `Version`(唯一版本来源)+ CHANGELOG 增加版本段; +2. 提交 `chore(release): x.y.z(一句话)`; +3. `git tag x.y.z && git push origin x.y.z` → `Release` 工作流产出 6 目标二进制; +4. 发布会校验 tag 与 `version.go` 一致,不一致直接失败。