Skip to content

Commit e2e67f9

Browse files
gqcnclaude
andcommitted
refactor(workflows): migrate nightly openspec archive to monthly and enforce cross-platform tooling
Rename nightly-openspec-archive workflows and actions to monthly cadence. Replace platform-specific shell scripts with Go-based linactl test-go. Add reusable CI workflows for e2e tests, host-only build smoke, image publish, and redis cluster smoke. Update service dependency injection to use explicit construction and add submodule guards for plugin tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent efe74d2 commit e2e67f9

61 files changed

Lines changed: 2005 additions & 1952 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/lina-review/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,18 @@ compatibility: 依赖 OpenSpec CLI、GoFrame v2 技能、lina-e2e 技能。
241241
5. 验证缓存更新与成功的权威数据源写入耦合,而非在事务仍可能回滚前发出。遗漏的失效事件、进程重启和过期的分布式条目必须有恢复路径(如 TTL、版本检查、重建/穿透读取、对账或显式重载)。
242242
6. 测试或审查证据应在适当的风险级别覆盖变更的缓存行为,特别是单机与集群模式分支、多实例失效、有界陈旧性、重试/重建行为,以及缓存后端不可用时的面向调用方行为。
243243

244+
#### 开发工具与脚本跨平台审查
245+
246+
**触发条件**:任何修改 `Makefile``make.cmd``hack/makefiles/``hack/scripts/``hack/tests/scripts/``hack/tools/``.github/workflows/` 中开发/构建/测试入口,或新增/修改 `.sh``.ps1``.cmd``.mjs`、工具型 `Go` 代码的变更
247+
248+
对每个开发工具或脚本变更,还需执行**跨平台执行审查**
249+
1. 默认开发、构建、测试、代码生成、资源打包、服务启停、CI 辅助和仓库治理入口必须能在 `Windows``Linux``macOS` 上运行。标记依赖单一平台默认命令或语义的实现,例如 `bash``sh``sed``awk``grep``perl``lsof``pgrep``xargs``kill``rm``cp``mv``mkdir -p`、POSIX 路径分隔符、Unix 信号或 PowerShell 专属语法。
250+
2. 长期维护的仓库工具应优先实现为 `Go` 工具,并通过 `go run ./hack/tools/<tool>``linactl` 或薄包装入口调用。审查是否把文件复制、目录遍历、配置改写、进程启停、端口探测、HTTP smoke、压缩/解压、模板渲染和静态扫描等逻辑留在 Shell 管道中;能合理迁移到 Go 标准库或已有 Go 组件的,应报告为问题。
251+
3. `Makefile``make.cmd` 只能作为兼容包装层,不得承载复杂业务逻辑;二者应委托 `linactl` 或其他跨平台工具。标记在包装层新增不可移植命令、平台专属环境写法或与 Go workspace/plugin workspace 规则冲突的隐式覆盖。
252+
4. `hack/scripts/` 不应继续承载长期维护脚本。新增或保留 `.sh``.ps1` 等平台脚本时,审查结论必须说明无法使用 Go 工具链的原因、受支持平台、等价跨平台入口和验证方式;没有说明的平台脚本应标记为严重问题。
253+
5. 工具变更必须提供匹配验证证据,例如 `cd hack/tools/linactl && go test ./... -count=1``go run ./hack/tools/linactl test-scripts`、目标 Go 工具单元测试、跨平台 smoke 或静态扫描。仅使用 `bash -n`、本机 Shell 试跑或单平台 workflow 不能证明跨平台合规。
254+
6. 如果变更无开发工具或脚本影响,审查结论应明确说明该判断。
255+
244256
### 6. SQL 规范审查
245257

246258
**触发条件**`apps/lina-core/manifest/sql/``apps/lina-core/manifest/sql/mock-data/``apps/lina-plugins/**/manifest/sql/` 下新增或修改的文件,或相关交付文档中嵌入的 `SQL` 片段
@@ -304,6 +316,9 @@ compatibility: 依赖 OpenSpec CLI、GoFrame v2 技能、lina-e2e 技能。
304316
### 分布式缓存一致性审查
305317
✓ 已审查分布式缓存一致性 / ⚠ 发现 N 个缓存一致性问题
306318

319+
### 开发工具与脚本跨平台审查
320+
✓ 开发工具和脚本跨平台合规 / ⚠ 发现 N 个跨平台问题
321+
307322
### SQL 审查
308323
✓ 无 SQL 变更 / ✓ SQL 变更合规 / ⚠ 发现 N 个 SQL 问题
309324

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Host-only artifact smoke test composite action.
2+
# Boots the compiled backend binary in single-node mode and validates health, login, and plugin endpoints.
3+
# 宿主模式构建产物冒烟测试复合操作。
4+
# 以单节点模式启动编译后的后端二进制文件,校验健康检查、登录和插件列表等核心接口。
5+
name: Host-only Artifact Smoke
6+
description: Start a host-only backend artifact in Linux GitHub Actions jobs and verify core endpoints.
7+
8+
# Path overrides let callers reuse this action for different build profiles.
9+
# 路径覆盖参数允许调用方为不同构建配置复用本操作。
10+
inputs:
11+
binary-path:
12+
description: Path to the built LinaPro backend artifact.
13+
required: false
14+
default: temp/host-only-output/lina
15+
log-path:
16+
description: Path for the backend smoke log.
17+
required: false
18+
default: temp/host-only-smoke/lina-core.log
19+
20+
runs:
21+
using: composite
22+
steps:
23+
# Start the binary, poll the health endpoint, then verify login and plugin list responses.
24+
# 启动二进制文件,轮询健康检查接口,然后验证登录和插件列表响应。
25+
- name: Run Host-only Artifact Smoke
26+
shell: bash
27+
env:
28+
BINARY_PATH: ${{ inputs.binary-path }}
29+
LOG_PATH: ${{ inputs.log-path }}
30+
run: |
31+
# Resolve relative paths against the workspace root.
32+
# 将相对路径解析为基于工作区根目录的绝对路径。
33+
case "${BINARY_PATH}" in
34+
/*) binary_abs="${BINARY_PATH}" ;;
35+
*) binary_abs="${GITHUB_WORKSPACE}/${BINARY_PATH}" ;;
36+
esac
37+
case "${LOG_PATH}" in
38+
/*) log_abs="${LOG_PATH}" ;;
39+
*) log_abs="${GITHUB_WORKSPACE}/${LOG_PATH}" ;;
40+
esac
41+
42+
# Ensure the log directory exists and register a cleanup trap for the backend process.
43+
# 确保日志目录存在,并注册清理钩子以在脚本退出时终止后端进程。
44+
mkdir -p "$(dirname "${log_abs}")"
45+
backend_pid=""
46+
cleanup() {
47+
if [ -n "${backend_pid}" ] && kill -0 "${backend_pid}" 2>/dev/null; then
48+
kill "${backend_pid}" 2>/dev/null || true
49+
wait "${backend_pid}" 2>/dev/null || true
50+
fi
51+
}
52+
trap cleanup EXIT INT TERM
53+
54+
# Launch the backend binary from the lina-core app directory, redirecting output to the log file.
55+
# 从 lina-core 应用目录启动后端二进制文件,将输出重定向到日志文件。
56+
pushd apps/lina-core >/dev/null
57+
"${binary_abs}" > "${log_abs}" 2>&1 &
58+
backend_pid="$!"
59+
popd >/dev/null
60+
61+
# Poll the health endpoint for up to 90 seconds, expecting single-node host-only mode.
62+
# 轮询健康检查接口最多 90 秒,期望返回单节点宿主模式。
63+
response=""
64+
for _ in $(seq 1 90); do
65+
if ! kill -0 "${backend_pid}" 2>/dev/null; then
66+
echo "Host-only backend exited before health check passed" >&2
67+
echo "Check log: ${LOG_PATH}" >&2
68+
exit 1
69+
fi
70+
response="$(curl -fsS "http://127.0.0.1:8080/api/v1/health" 2>/dev/null || true)"
71+
if [ -n "${response}" ] && HEALTH_RESPONSE="${response}" python3 - <<'PY'
72+
import json
73+
import os
74+
import sys
75+
76+
payload = json.loads(os.environ["HEALTH_RESPONSE"])
77+
data = payload.get("data", payload)
78+
if payload.get("code", 0) == 0 and data.get("status") == "ok" and data.get("mode") == "single":
79+
sys.exit(0)
80+
sys.exit(1)
81+
PY
82+
then
83+
break
84+
fi
85+
sleep 1
86+
done
87+
88+
# Fail fast if the health endpoint never responded.
89+
# 如果健康检查接口始终未响应则立即失败。
90+
if [ -z "${response}" ]; then
91+
echo "Host-only backend did not return a health response" >&2
92+
echo "Check log: ${LOG_PATH}" >&2
93+
exit 1
94+
fi
95+
96+
# Validate the final health response payload in detail.
97+
# 详细校验最终的健康检查响应载荷。
98+
HEALTH_RESPONSE="${response}" python3 - <<'PY'
99+
import json
100+
import os
101+
102+
payload = json.loads(os.environ["HEALTH_RESPONSE"])
103+
data = payload.get("data", payload)
104+
if payload.get("code", 0) != 0:
105+
raise SystemExit(f"health business code is not zero: {payload!r}")
106+
if data.get("status") != "ok":
107+
raise SystemExit(f"expected health status ok, got: {payload!r}")
108+
if data.get("mode") != "single":
109+
raise SystemExit(f"expected single-node host-only mode, got: {payload!r}")
110+
PY
111+
112+
# Authenticate with the default admin account and extract the access token.
113+
# 使用默认管理员账号登录并提取访问令牌。
114+
login_response="$(
115+
curl -fsS \
116+
-H "Content-Type: application/json" \
117+
-d '{"username":"admin","password":"admin123"}' \
118+
"http://127.0.0.1:8080/api/v1/auth/login"
119+
)"
120+
token="$(LOGIN_RESPONSE="${login_response}" python3 - <<'PY'
121+
import json
122+
import os
123+
124+
payload = json.loads(os.environ["LOGIN_RESPONSE"])
125+
data = payload.get("data", payload)
126+
if payload.get("code", 0) != 0:
127+
raise SystemExit(f"login business code is not zero: {payload!r}")
128+
token = data.get("accessToken")
129+
if not token:
130+
raise SystemExit(f"login response did not contain accessToken: {payload!r}")
131+
print(token)
132+
PY
133+
)"
134+
135+
# Verify that no source plugins are loaded in host-only mode.
136+
# 验证宿主模式下未加载任何源码插件。
137+
plugins_response="$(
138+
curl -fsS \
139+
-H "Authorization: Bearer ${token}" \
140+
"http://127.0.0.1:8080/api/v1/plugins?type=source"
141+
)"
142+
PLUGINS_RESPONSE="${plugins_response}" python3 - <<'PY'
143+
import json
144+
import os
145+
146+
payload = json.loads(os.environ["PLUGINS_RESPONSE"])
147+
data = payload.get("data", payload)
148+
if payload.get("code", 0) != 0:
149+
raise SystemExit(f"plugin list business code is not zero: {payload!r}")
150+
plugin_items = data.get("list") or []
151+
if plugin_items:
152+
raise SystemExit(f"expected no source plugins in host-only artifact, got: {plugin_items!r}")
153+
if data.get("total", 0) != 0:
154+
raise SystemExit(f"expected source plugin total=0 in host-only artifact, got: {payload!r}")
155+
PY
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Detect whether the monthly OpenSpec archive task produced workspace changes.
2+
# Outputs a boolean flag consumed by downstream steps to decide whether to open a PR.
3+
# 检测月度 OpenSpec 归档任务是否产生了工作区变更。
4+
# 输出布尔标志供下游步骤判断是否需要创建 Pull Request。
5+
name: Monthly OpenSpec Detect Changes
6+
description: Detect whether monthly auto archive produced OpenSpec changes.
7+
8+
outputs:
9+
changed:
10+
description: Whether the current workspace has OpenSpec changes.
11+
value: ${{ steps.archive-diff.outputs.changed }}
12+
13+
runs:
14+
using: composite
15+
steps:
16+
# Compare the openspec directory against HEAD; any diff means changes were produced.
17+
# 将 openspec 目录与 HEAD 进行比较,存在差异即表示产生了变更。
18+
- name: Detect Archive Changes
19+
id: archive-diff
20+
shell: bash
21+
run: |
22+
if git diff --quiet -- openspec; then
23+
echo "changed=false" >> "${GITHUB_OUTPUT}"
24+
echo "Auto archive produced no OpenSpec changes."
25+
else
26+
echo "changed=true" >> "${GITHUB_OUTPUT}"
27+
echo "Auto archive produced OpenSpec changes."
28+
fi

.github/actions/nightly-openspec-finalize-pr/action.yml renamed to .github/actions/monthly-openspec-finalize-pr/action.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
name: Nightly OpenSpec Finalize Pull Request
1+
# Validate, scope-guard, and create or update the monthly OpenSpec archive pull request.
2+
# Runs openspec validate, ensures only openspec/ files changed, then commits and pushes a PR.
3+
# 校验、作用域守护并创建或更新月度 OpenSpec 归档 Pull Request。
4+
# 运行 openspec validate,确保仅有 openspec/ 文件变更,然后提交并推送 Pull Request。
5+
name: Monthly OpenSpec Finalize Pull Request
26
description: Validate generated OpenSpec changes, guard their scope, and create or update the archive pull request.
37

8+
# changed: skip all work when the archive step produced no diff.
9+
# openspec-version: pinned CLI version for reproducible validation.
10+
# base-branch / pr-branch / pr-title: PR targeting and commit metadata.
11+
# changed: 当归档步骤未产生差异时跳过所有后续工作。
12+
# openspec-version: 固定 CLI 版本以保证校验结果可重现。
13+
# base-branch / pr-branch / pr-title: PR 目标分支和提交元数据。
414
inputs:
515
changed:
6-
description: Whether the nightly archive task produced OpenSpec changes.
16+
description: Whether the monthly archive task produced OpenSpec changes.
717
required: true
818
openspec-version:
919
description: OpenSpec CLI version to run.
@@ -14,7 +24,7 @@ inputs:
1424
pr-branch:
1525
description: Pull request head branch.
1626
required: false
17-
default: automation/nightly-openspec-archive
27+
default: automation/monthly-openspec-archive
1828
pr-title:
1929
description: Pull request title and commit message.
2030
required: false
@@ -23,6 +33,8 @@ inputs:
2333
runs:
2434
using: composite
2535
steps:
36+
# Run the OpenSpec validator against all change manifests.
37+
# 对所有变更清单运行 OpenSpec 校验器。
2638
- name: Validate OpenSpec
2739
if: ${{ inputs.changed == 'true' }}
2840
shell: bash
@@ -31,6 +43,8 @@ runs:
3143
run: |
3244
npx -y "@fission-ai/openspec@${OPENSPEC_VERSION}" validate --all
3345
46+
# Collect all modified and untracked files, then reject anything outside openspec/.
47+
# 收集所有修改和未跟踪的文件,然后拒绝 openspec/ 之外的任何变更。
3448
- name: Guard Generated Changes
3549
shell: bash
3650
run: |
@@ -61,6 +75,8 @@ runs:
6175
console.log('Generated file changes are within the allowed OpenSpec scope.');
6276
NODE
6377
78+
# Commit the archived changes and create or update the PR via GitHub CLI.
79+
# 提交归档变更并通过 GitHub CLI 创建或更新 Pull Request。
6480
- name: Create or Update Archive Pull Request
6581
if: ${{ inputs.changed == 'true' }}
6682
shell: bash
@@ -72,29 +88,37 @@ runs:
7288
run: |
7389
set -euo pipefail
7490
91+
# Configure the bot identity for automated commits.
92+
# 为自动化提交配置机器人身份。
7593
git config user.name "github-actions[bot]"
7694
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
7795
7896
git switch -c "${PR_BRANCH}"
7997
git add openspec
8098
99+
# Exit early if there is nothing new to commit.
100+
# 如果没有新的变更需要提交则提前退出。
81101
if git diff --cached --quiet; then
82102
echo "No staged archive changes to include in the pull request."
83103
exit 0
84104
fi
85105
86106
git commit -m "${PR_TITLE}"
87107
108+
# Push with force-with-lease when the remote branch already exists, otherwise push normally.
109+
# 远程分支已存在时使用 force-with-lease 推送,否则正常推送。
88110
remote_sha="$(git ls-remote --heads origin "${PR_BRANCH}" | awk '{print $1}')"
89111
if [ -n "${remote_sha}" ]; then
90112
git push --force-with-lease="refs/heads/${PR_BRANCH}:${remote_sha}" origin "HEAD:${PR_BRANCH}"
91113
else
92114
git push origin "HEAD:${PR_BRANCH}"
93115
fi
94116
95-
pr_body="${RUNNER_TEMP}/nightly-openspec-archive-pr.md"
117+
# Prepare the PR body describing the automated archive contents.
118+
# 准备描述自动归档内容的 PR 正文。
119+
pr_body="${RUNNER_TEMP}/monthly-openspec-archive-pr.md"
96120
cat > "${pr_body}" <<'EOF'
97-
This PR was created by the nightly OpenSpec archive workflow.
121+
This PR was created by the monthly OpenSpec archive workflow.
98122
99123
It contains only `openspec/**` archive and archive consolidation updates.
100124
@@ -103,6 +127,8 @@ runs:
103127
- generated change scope guard for `openspec/**`
104128
EOF
105129
130+
# Check for an existing open PR; update it if found, otherwise create a new one.
131+
# 检查是否已存在打开的 PR,如存在则更新,否则创建新的。
106132
pr_number="$(gh pr list --head "${PR_BRANCH}" --state open --json number --jq '.[0].number // ""')"
107133
if [ -n "${pr_number}" ]; then
108134
gh pr edit "${pr_number}" --title "${PR_TITLE}" --body-file "${pr_body}" --base "${BASE_BRANCH}"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Prepare the shared runner environment for monthly OpenSpec workflows.
2+
# Sets the timezone to Asia/Shanghai and installs the required Node.js version.
3+
# 为月度 OpenSpec 工作流准备共享 Runner 环境。
4+
# 将时区设置为 Asia/Shanghai 并安装所需的 Node.js 版本。
5+
name: Monthly OpenSpec Setup
6+
description: Prepare the shared runner environment after repository checkout.
7+
8+
runs:
9+
using: composite
10+
steps:
11+
# Align the runner clock with the project timezone for consistent log timestamps.
12+
# 将 Runner 时钟与项目时区对齐,确保日志时间戳一致。
13+
- name: Setup Timezone
14+
shell: bash
15+
run: |
16+
sudo ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
17+
echo Asia/Shanghai | sudo tee /etc/timezone
18+
19+
# Install Node.js for running the OpenSpec CLI via npx.
20+
# 安装 Node.js 以便通过 npx 运行 OpenSpec CLI。
21+
- name: Setup Node
22+
uses: actions/setup-node@v6
23+
with:
24+
node-version: "23"

.github/actions/nightly-openspec-detect-changes/action.yml

Lines changed: 0 additions & 22 deletions
This file was deleted.

.github/actions/nightly-openspec-setup/action.yml

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)