🔧 chore(workflows): migrate to prepare + worktree model and merge checks

- add prepare.yml: install tools + maintain shared bare repo, anchors the workflow_run chain
- add checks.yml: merge lint + full test suite into a single workflow_run consumer job
- remove standards-check.yml and test.yml (superseded by checks.yml)
- convert sync-tsl-playbook and update-thirdparty-skills to worktree-model consumers
- unify workflow/job/step display names and emoji
This commit is contained in:
csh
2026-07-21 17:29:24 +08:00
parent 49e6dadc60
commit d83707de23
7 changed files with 600 additions and 267 deletions
+157
View File
@@ -0,0 +1,157 @@
name: 🔎 校验
on:
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: checks-${{ github.repository }}-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: true
# ==========================================
# 🔧 配置区域 - 校验参数
# ==========================================
env:
# Prepare 完成后经 workflow_run 触发;Prepare 已装工具 + fetch 到共享 bare 仓库。
# 本 workflow 只做只读校验(lint + 测试),不推任何分支,故 token 仅用于
# 极端情况下补取缺失提交(正常路径 Prepare 已 fetch 到位)。
#
# 安全:ACCESS_TOKEN 不放在 workflow 级 env。lint/test 步骤会执行上游提交
# (PR 场景为不受信输入)的脚本,其环境里不应出现 token;仅下放到
# prepare_worktree 步骤。
WORKSPACE_ROOT: "/data/workspace"
SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }}
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
SOURCE_EVENT: ${{ github.event.workflow_run.event }}
COMMIT_LINT_REQUIRE_EMOJI: "1"
jobs:
checks:
# 仅在 Prepare 成功后触发。Prepare 覆盖 push(main) / pull_request /
# schedule / dispatch,故 checks 在这些场景均会运行。
# lint 与 test 合并为单 job:共用一个临时 worktree,准备一次,顺序执行
# lint 在前 fail-fast,失败即不跑 test)。二者跑同一 SOURCE_SHA、皆只读、
# 无依赖,无需拆分为并行 job(那会重复整段 worktree 准备逻辑 + 多一个 worktree)。
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: 🔎 规范检查 + 全量测试
runs-on: standard-ubuntu-22
permissions:
contents: read
steps:
- name: 📥 准备临时 Worktree
id: prepare_worktree
shell: bash
env:
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
run: |
set -euo pipefail
REPO_NAME="${{ github.event.repository.name }}"
HEAD_SHA="${SOURCE_SHA}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/checks-${{ github.run_id }}-${{ github.run_attempt }}"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
if [ ! -d "$REPOSITORY_DIR" ] || \
[ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then
echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
# 正常路径 Prepare 已把上游提交 fetch 到位;极端情况下(bare 仓库被清理
# 重建、或 Prepare 与本 run 之间发生 prune)补取一次,token 仅用于此。
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then
git \
-c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
-c credential.interactive=never \
--git-dir="$REPOSITORY_DIR" fetch --prune --force origin \
'+refs/heads/*:refs/remotes/origin/*' || true
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在上游提交: $HEAD_SHA(事件: $SOURCE_EVENT" >&2
exit 1
fi
exec 9>"$WORKTREE_LOCK"
flock 9
# 回收陈旧临时 worktree(超过 24h 的 checks-* 目录)。worktree prune 只清理
# 指向已消失目录的 metadata,不删仍存在的陈旧目录,故按 mtime 主动回收。
worktrees_root="$(dirname "$WORKTREE_DIR")"
if [ -d "$worktrees_root" ]; then
find "$worktrees_root" -mindepth 1 -maxdepth 1 -type d -name 'checks-*' \
-mmin +1440 -print 2>/dev/null | while IFS= read -r stale; do
[ "$stale" = "$WORKTREE_DIR" ] && continue
git --git-dir="$REPOSITORY_DIR" worktree remove --force "$stale" 2>/dev/null || rm -rf "$stale"
done
fi
git --git-dir="$REPOSITORY_DIR" worktree prune
if [ -e "$WORKTREE_DIR" ]; then
git --git-dir="$REPOSITORY_DIR" worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR"
fi
mkdir -p "$(dirname "$WORKTREE_DIR")"
git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA"
flock -u 9
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "✅ Worktree 就绪: $WORKTREE_DIR @ $HEAD_SHA"
- name: 🔍 校验提交信息
shell: bash
run: |
set -euo pipefail
cd "$REPO_DIR"
# 注:经 workflow_run 触发,事件 payload 为 workflow_run,不含 PR title
# lint 脚本会回落到 head commit message 校验(PR 标题本身不再校验)。
echo "🔍 检查 commit message 规范(上游事件: $SOURCE_EVENT..."
python3 .gitea/ci/commit_message_lint.py
echo "✅ Lint 检查通过"
- name: 🧪 运行全量测试
shell: bash
run: |
set -euo pipefail
echo "========================================"
echo "🐍 Python 测试(上游事件: $SOURCE_EVENT"
echo "========================================"
cd "$REPO_DIR"
echo "📋 覆盖:CLI、subtree/snapshot 部署路线、模板同步、文档一致性"
python3 -m unittest discover -s test -p "test_*.py" -v
echo "✅ Python 测试通过"
echo "========================================"
echo "🔗 文档链接检查"
echo "========================================"
python3 test/integration/check_doc_links.py
echo "✅ 文档链接检查通过"
echo "🎉 所有测试完成"
- name: 🧹 清理临时 Worktree
if: always()
shell: bash
run: |
set -uo pipefail
[ -n "${REPOSITORY_DIR:-}" ] || exit 0
[ -n "${REPO_DIR:-}" ] || exit 0
WORKTREE_LOCK="$(dirname "$REPOSITORY_DIR")/worktree-admin.lock"
exec 9>"$WORKTREE_LOCK"
flock 9
git --git-dir="$REPOSITORY_DIR" worktree remove --force "$REPO_DIR" 2>/dev/null || rm -rf "$REPO_DIR"
git --git-dir="$REPOSITORY_DIR" worktree prune
flock -u 9
+212
View File
@@ -0,0 +1,212 @@
name: 🧰 准备环境
on:
push:
# 主分支:下游 sync / thirdparty 消费者只关心 main。
# 其它分支(含 ci bot 推送的 tsl-playbook / thirdparty/skill 分支)跑 Prepare 纯属空耗,故在此过滤。
branches:
- main
pull_request:
# PR:为下游 checks 消费者(standards + tests)预备 bare 仓库。
# PR 的 github.sha 是 Gitea 生成的 merge commit,不在 refs/heads/*
# 故下面的 fetch 额外取 PR ref 并按事件类型选校验目标。
branches:
- main
workflow_dispatch:
schedule:
# thirdparty 快照每日轮询:Prepare 定时成功后,update-thirdparty-skills 通过 workflow_run 触发。
- cron: "17 3 * * *"
concurrency:
group: prepare-${{ github.repository }}
cancel-in-progress: false
env:
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
WORKSPACE_ROOT: "/data/workspace"
jobs:
prepare:
runs-on: standard-ubuntu-22
permissions:
contents: read
steps:
- name: 🔧 检查并安装共享工具
id: tools
shell: bash
run: |
set -euo pipefail
declare -A package_for=(
[git]="git"
[python3]="python3"
[curl]="curl"
[flock]="util-linux"
[realpath]="coreutils"
)
required_commands=(git python3 curl flock realpath)
missing_commands=()
missing_packages=()
declare -A seen_packages=()
for command in "${required_commands[@]}"; do
if ! command -v "$command" >/dev/null 2>&1; then
package="${package_for[$command]}"
missing_commands+=("$command")
if [[ -z "${seen_packages[$package]+x}" ]]; then
missing_packages+=("$package")
seen_packages[$package]=1
fi
fi
done
installed=false
if [ "${#missing_packages[@]}" -gt 0 ]; then
echo "缺失命令: ${missing_commands[*]}"
echo "准备安装: ${missing_packages[*]}"
if [ "$(id -u)" -eq 0 ]; then
APT=(apt-get)
elif command -v sudo >/dev/null 2>&1; then
APT=(sudo apt-get)
else
echo "无法安装缺失命令;当前用户不是 root 且没有 sudo" >&2
echo "命令: ${missing_commands[*]}" >&2
echo "包: ${missing_packages[*]}" >&2
exit 1
fi
"${APT[@]}" update -qq
"${APT[@]}" install -y -qq "${missing_packages[@]}"
installed=true
fi
for command in "${required_commands[@]}"; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "安装后仍找不到命令: $command (${package_for[$command]})" >&2
exit 1
fi
done
{
echo "## Prepare 环境"
echo ""
echo "| 命令 | 版本 |"
echo "| --- | --- |"
for command in "${required_commands[@]}"; do
version_output=$("$command" --version 2>&1 || true)
version_line=${version_output%%$'\n'*}
version_line=${version_line//|/\\|}
printf '| `%s` | `%s` |\n' "$command" "$version_line"
done
echo ""
if [ "$installed" = true ]; then
echo "安装的包:\`${missing_packages[*]}\`"
else
echo "安装的包:无,所有命令已存在。"
fi
} >> "$GITHUB_STEP_SUMMARY"
echo "installed=$installed" >> "$GITHUB_OUTPUT"
- name: 📥 准备共享 bare 仓库
shell: bash
run: |
set -euo pipefail
REPO_NAME="${{ github.event.repository.name }}"
REMOTE_URL="${{ github.server_url }}/${{ github.repository }}.git"
EVENT_NAME="${{ github.event_name }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
# 校验目标 SHA 因事件而异:
# - pull_requestgithub.sha 是 Gitea 生成的 merge commit,不在
# refs/heads/* 里,直接校验会失败;改用 PR head SHA,并在下方额外
# fetch PR ref 把 head 提交拉进 bare 仓库。下游 workflow_run 消费者
# 读到的 head_sha 也正是这个 PR head,两者一致。
# - 其它事件(push / schedule / dispatch):沿用 github.sha。
if [ "$EVENT_NAME" = "pull_request" ]; then
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
PR_NUMBER="${{ github.event.pull_request.number }}"
else
HEAD_SHA="${{ github.sha }}"
PR_NUMBER=""
fi
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的触发提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
case "$REMOTE_URL" in
https://*) ;;
*)
echo "拒绝通过非 HTTPS remote 使用 WORKFLOW token: $REMOTE_URL" >&2
exit 1
;;
esac
if [ -z "$ACCESS_TOKEN" ]; then
echo "未配置 WORKFLOW secret,无法读取仓库" >&2
exit 1
fi
mkdir -p "$(dirname "$REPOSITORY_DIR")"
if [ ! -e "$REPOSITORY_DIR" ]; then
git init --bare "$REPOSITORY_DIR"
else
if [ ! -d "$REPOSITORY_DIR" ]; then
echo "共享仓库路径存在但不是目录: $REPOSITORY_DIR" >&2
exit 1
fi
if ! is_bare=$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null) || \
[ "$is_bare" != "true" ]; then
echo "共享仓库路径不是有效 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
fi
if git --git-dir="$REPOSITORY_DIR" remote get-url origin >/dev/null 2>&1; then
git --git-dir="$REPOSITORY_DIR" remote set-url origin "$REMOTE_URL"
else
git --git-dir="$REPOSITORY_DIR" remote add origin "$REMOTE_URL"
fi
git \
-c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
-c credential.interactive=never \
--git-dir="$REPOSITORY_DIR" fetch \
--prune --prune-tags --tags --force origin \
'+refs/heads/*:refs/remotes/origin/*'
# PR head 不在 refs/heads/*,按需补取 PR ref 把 head 提交拉进 bare 仓库。
# 先试 PR head SHA 是否已在库(同仓库分支 PR 的 head 常已随 refs/heads/*
# 到位),不在再按 PR 号 fetch Gitea 的 refs/pull/<n>/head。
if [ -n "$PR_NUMBER" ] && \
! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then
git \
-c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
-c credential.interactive=never \
--git-dir="$REPOSITORY_DIR" fetch --force origin \
"+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pull/${PR_NUMBER}/head" || true
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在触发提交: $HEAD_SHA(事件: $EVENT_NAME" >&2
exit 1
fi
resolved_sha=$(git --git-dir="$REPOSITORY_DIR" rev-parse "${HEAD_SHA}^{commit}")
{
echo ""
echo "## Prepare 仓库"
echo ""
echo "- bare 仓库:\`$REPOSITORY_DIR\`"
echo "- 触发提交:\`$resolved_sha\`"
echo "- 远端:\`$REMOTE_URL\`"
} >> "$GITHUB_STEP_SUMMARY"
echo "Prepare 完成: $REPOSITORY_DIR @ $resolved_sha"
-75
View File
@@ -1,75 +0,0 @@
name: ✅ Standards Check
on:
push:
pull_request:
workflow_dispatch: # 允许手动触发
concurrency:
group: standards-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
# ==========================================
# 🔧 配置区域 - 标准校验参数
# ==========================================
env:
COMMIT_LINT_REQUIRE_EMOJI: "1"
WORKSPACE_DIR: "/home/workspace"
jobs:
commit-message:
name: 🔍 Commit message lint
runs-on: ubuntu-22.04
steps:
- name: 📥 准备仓库
run: |
set -euo pipefail
echo "========================================"
echo "📦 准备仓库到 WORKSPACE_DIR"
echo "========================================"
REPO_NAME="${{ github.event.repository.name }}"
TOKEN="${{ secrets.WORKFLOW }}"
TARGET_SHA="${{ github.sha }}"
TARGET_REF="${{ github.ref }}"
TARGET_REF_NAME="${{ github.ref_name }}"
mkdir -p "$WORKSPACE_DIR"
REPO_DIR="$(mktemp -d "$WORKSPACE_DIR/${REPO_NAME}.XXXXXX")"
if [ -n "$TOKEN" ]; then
REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
else
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
fi
git clone "$REPO_URL" "$REPO_DIR"
if git -C "$REPO_DIR" cat-file -e "${TARGET_SHA}^{commit}" 2>/dev/null; then
git -C "$REPO_DIR" checkout -f "$TARGET_SHA"
else
if [ -n "$TARGET_REF" ]; then
git -C "$REPO_DIR" fetch origin "$TARGET_REF"
git -C "$REPO_DIR" checkout -f FETCH_HEAD
else
git -C "$REPO_DIR" checkout -f "$TARGET_REF_NAME"
fi
fi
git config --global --add safe.directory "$REPO_DIR"
echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
echo "✅ 仓库准备完成"
- name: 🔍 Lint commit message / PR title
run: |
cd "$REPO_DIR"
echo "🔍 检查 commit message 和 PR title 规范..."
python3 .gitea/ci/commit_message_lint.py
echo "✅ Lint 检查通过"
- name: 🧹 清理临时仓库
if: always()
run: |
rm -rf "$REPO_DIR"
+117 -55
View File
@@ -1,78 +1,130 @@
name: 📦 Sync TSL Playbook
name: 📦 发布 TSL Playbook
on:
push:
branches:
- main
workflow_dispatch:
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: sync-tsl-playbook-${{ github.repository }}
cancel-in-progress: true
cancel-in-progress: false
# ==========================================
# 🔧 配置区域
# ==========================================
env:
WORKSPACE_DIR: "/home/workspace"
# Prepare 维护共享仓库并读取代码;此处 token 仅用于推送。
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
# ===== 工作区配置 =====
WORKSPACE_ROOT: "/data/workspace"
WORKSPACE_SLOT: "tsl-playbook"
SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }}
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
# ===== 分支与构建配置 =====
TARGET_BRANCH: "tsl-playbook"
BUILD_SCRIPT: "scripts/build_tsl_playbook.py"
# ===== Git 配置 =====
GIT_USER_NAME: "ci[bot]"
GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn"
jobs:
sync:
name: 📦 Build and publish tsl-playbook
runs-on: ubuntu-22.04
# 仅在 Prepare 于主分支成功后触发。
if: ${{ github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') }}
name: 🔨 构建并发布 tsl-playbook
runs-on: standard-ubuntu-22
permissions:
contents: write
steps:
- name: 📥 准备仓库
- name: 🔐 验证 Token 配置
shell: bash
env:
WORKFLOW: ${{ secrets.WORKFLOW }}
run: |
set -euo pipefail
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ 未配置 WORKFLOW secret,无法推送 $TARGET_BRANCH" >&2
exit 1
fi
echo "✅ Token 已配置"
echo "🌿 上游分支: ${{ env.SOURCE_BRANCH }}"
echo "📝 上游提交: ${{ env.SOURCE_SHA }}"
echo "========================================"
echo "📦 准备仓库到 WORKSPACE_DIR"
echo "========================================"
- name: 📥 对齐 Worktree
id: prepare_worktree
shell: bash
run: |
set -euo pipefail
REPO_NAME="${{ github.event.repository.name }}"
mkdir -p "$WORKSPACE_DIR"
REPO_DIR="$(mktemp -d "$WORKSPACE_DIR/${REPO_NAME}.XXXXXX")"
export GIT_TERMINAL_PROMPT=0
echo "GIT_TERMINAL_PROMPT=0" >> "$GITHUB_ENV"
HEAD_SHA="${{ env.SOURCE_SHA }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/${WORKSPACE_SLOT}"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
if [ -n "${WORKFLOW:-}" ]; then
ASKPASS_SCRIPT="$(mktemp "$WORKSPACE_DIR/git-askpass.XXXXXX")"
cat > "$ASKPASS_SCRIPT" <<'EOF'
#!/usr/bin/env sh
case "$1" in
*Username*) printf '%s\n' oauth2 ;;
*Password*) printf '%s\n' "$WORKFLOW" ;;
*) printf '\n' ;;
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
EOF
chmod 700 "$ASKPASS_SCRIPT"
export GIT_ASKPASS="$ASKPASS_SCRIPT"
echo "GIT_ASKPASS=$ASKPASS_SCRIPT" >> "$GITHUB_ENV"
if [ ! -d "$REPOSITORY_DIR" ] || \
[ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then
echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2
exit 1
fi
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -f "$WORKTREE_DIR/.git" ]; then
exec 9>"$WORKTREE_LOCK"
flock 9
git clone "$REPO_URL" "$REPO_DIR"
if [ ! -f "$WORKTREE_DIR/.git" ]; then
if [ -e "$WORKTREE_DIR" ]; then
echo "发布 Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2
exit 1
fi
mkdir -p "$(dirname "$WORKTREE_DIR")"
git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA"
fi
git -C "$REPO_DIR" fetch origin main
git -C "$REPO_DIR" checkout -B main origin/main
flock -u 9
fi
git config --global --add safe.directory "$REPO_DIR"
echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
echo "✅ 仓库准备完成"
common_dir=$(git -C "$WORKTREE_DIR" rev-parse --path-format=absolute --git-common-dir)
if [ "$common_dir" != "$(realpath "$REPOSITORY_DIR")" ]; then
echo "发布 Worktree 不属于共享仓库: $WORKTREE_DIR" >&2
exit 1
fi
- name: 📦 Build and publish tsl-playbook
git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA"
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
actual_sha=$(git -C "$WORKTREE_DIR" rev-parse HEAD)
if [ "$actual_sha" != "$HEAD_SHA" ]; then
echo "发布 Worktree 提交不匹配: 期望 $HEAD_SHA,实际 $actual_sha" >&2
exit 1
fi
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV"
echo "发布 Worktree: $WORKTREE_DIR @ $actual_sha"
- name: 📦 构建并发布 tsl-playbook
shell: bash
env:
WORKFLOW: ${{ secrets.WORKFLOW }}
run: |
set -euo pipefail
REPO_DIR="${REPO_DIR:-$(pwd)}"
REPO_DIR="${REPO_DIR}"
REPOSITORY_DIR="${REPOSITORY_DIR}"
TARGET_BRANCH="${TARGET_BRANCH:-tsl-playbook}"
BUILD_SCRIPT="${BUILD_SCRIPT:-scripts/build_tsl_playbook.py}"
@@ -85,10 +137,15 @@ jobs:
git config user.name "$GIT_USER_NAME"
git config user.email "$GIT_USER_EMAIL"
source_sha="$(git rev-parse HEAD)"
# 校验仍在上游提交,构建的是正确源码。
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "${{ env.SOURCE_SHA }}" ]; then
echo "源码 Worktree 不在上游提交: 期望 ${{ env.SOURCE_SHA }},实际 $actual_sha" >&2
exit 1
fi
source_short="$(git rev-parse --short HEAD)"
# Build the bundle outside the repo so branch checkout cannot clobber it.
# 在仓库外构建 bundle,切换分支时不会被清理。
build_dir="$(mktemp -d)"
cleanup() {
rm -rf "$build_dir"
@@ -98,7 +155,7 @@ jobs:
bundle="$build_dir/tsl-playbook"
python3 "$BUILD_SCRIPT" --output "$bundle"
# These are the only paths this workflow owns on the target branch.
# 本 workflow 在目标分支上仅拥有以下路径。
managed_paths=(
"AGENTS.md"
"docs/tsl"
@@ -113,11 +170,11 @@ jobs:
fi
done
if git show-ref --verify --quiet "refs/remotes/origin/$TARGET_BRANCH"; then
git fetch origin "$TARGET_BRANCH"
git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"
# 目标分支的引用由 Prepare fetch 到共享仓库;此处无需再 fetch。
if git --git-dir="$REPOSITORY_DIR" show-ref --verify --quiet "refs/remotes/origin/$TARGET_BRANCH"; then
git checkout --force -B "$TARGET_BRANCH" "refs/remotes/origin/$TARGET_BRANCH"
else
# A new orphan branch inherits main's index; clear it without deleting files.
# 目标分支尚不存在:创建孤儿分支,清空索引但保留工作树文件。
git checkout --orphan "$TARGET_BRANCH"
git rm -rf --cached --quiet . >/dev/null 2>&1 || true
fi
@@ -137,15 +194,20 @@ jobs:
git commit -m ":package: deps(tsl): sync tsl-playbook from ${source_short}
Source-Commit: ${source_sha}"
Source-Commit: ${{ env.SOURCE_SHA }}"
git push origin "$TARGET_BRANCH"
git -c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
-c credential.interactive=never \
push origin "$TARGET_BRANCH"
echo "✅ Published tsl-playbook @ ${source_short}"
- name: 🧹 清理临时仓库
- name: 🧹 重置 Worktree
if: always()
shell: bash
run: |
if [ -n "${GIT_ASKPASS:-}" ]; then
rm -f "$GIT_ASKPASS"
# 持久化 worktree 由下次运行前的 detached checkout + reset + clean 复用,
# 此处仅在结束时 detach,避免留下已检出的分支阻塞其它 worktree。
if [ -n "${REPO_DIR:-}" ] && [ -f "$REPO_DIR/.git" ]; then
git -C "$REPO_DIR" checkout --detach --force "${SOURCE_SHA}" 2>/dev/null || true
fi
rm -rf "$REPO_DIR"
-99
View File
@@ -1,99 +0,0 @@
---
name: 🧪 Playbook 测试套件
"on":
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch: # 允许手动触发
concurrency:
group: test-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
# ==========================================
# 🔧 配置区域 - 测试参数
# ==========================================
env:
# 测试工作目录
WORKSPACE_DIR: "/home/workspace"
jobs:
# ==========================================
# Job: 全量测试
# ==========================================
test:
name: 🧪 全量测试
runs-on: ubuntu-22.04
steps:
- name: 📥 准备仓库
run: |
set -euo pipefail
echo "========================================"
echo "📦 准备仓库到 WORKSPACE_DIR"
echo "========================================"
REPO_NAME="${{ github.event.repository.name }}"
TOKEN="${{ secrets.WORKFLOW }}"
TARGET_SHA="${{ github.sha }}"
TARGET_REF="${{ github.ref }}"
TARGET_REF_NAME="${{ github.ref_name }}"
mkdir -p "$WORKSPACE_DIR"
REPO_DIR="$(mktemp -d "$WORKSPACE_DIR/${REPO_NAME}.XXXXXX")"
if [ -n "$TOKEN" ]; then
REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
else
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
fi
git clone "$REPO_URL" "$REPO_DIR"
if git -C "$REPO_DIR" cat-file -e \
"${TARGET_SHA}^{commit}" 2>/dev/null; then
git -C "$REPO_DIR" checkout -f "$TARGET_SHA"
else
if [ -n "$TARGET_REF" ]; then
git -C "$REPO_DIR" fetch origin "$TARGET_REF"
git -C "$REPO_DIR" checkout -f FETCH_HEAD
else
git -C "$REPO_DIR" checkout -f "$TARGET_REF_NAME"
fi
fi
git config --global --add safe.directory "$REPO_DIR"
echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
echo "✅ 仓库准备完成"
- name: 🧪 运行全量测试
shell: bash
run: |
set -euo pipefail
echo "========================================"
echo "🐍 Python 测试"
echo "========================================"
cd "$REPO_DIR"
echo "📋 覆盖:CLI、subtree/snapshot 部署路线、模板同步、文档一致性"
python3 -m unittest discover -s test -p "test_*.py" -v
echo "✅ Python 测试通过"
echo "========================================"
echo "🔗 文档链接检查"
echo "========================================"
python3 test/integration/check_doc_links.py
echo "✅ 文档链接检查通过"
echo "🎉 所有测试完成"
- name: 🧹 清理临时仓库
if: always()
run: |
rm -rf "$REPO_DIR"
+108 -32
View File
@@ -1,19 +1,22 @@
name: 🪄 Update Third-Party Skills
name: ♻️ 更新第三方 Skills
on:
push:
branches:
- main
workflow_dispatch:
schedule:
- cron: "@daily"
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: update-thirdparty-${{ github.repository }}
cancel-in-progress: true
cancel-in-progress: false
env:
WORKSPACE_DIR: "/home/workspace"
# Prepare 维护共享仓库并读取代码;此处 token 用于 worktree 内的 fetch/push。
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
WORKSPACE_ROOT: "/data/workspace"
WORKSPACE_SLOT: "thirdparty"
SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }}
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
THIRDPARTY_BRANCH: "thirdparty/skill"
MANIFEST_PATH: ".gitea/ci/thirdparty_skills.json"
GIT_USER_NAME: "ci[bot]"
@@ -21,39 +24,107 @@ env:
jobs:
update_and_sync:
name: ♻️ Update thirdparty and sync main
runs-on: ubuntu-22.04
# 仅在 Prepare 于主分支成功后触发(含每日 schedule 触发的 Prepare)。
if: ${{ github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') }}
name: 📥 更新快照并同步 main
runs-on: standard-ubuntu-22
permissions:
contents: write
steps:
- name: 🧰 Prepare repo
- name: 🔐 验证 Token 配置
shell: bash
run: |
set -euo pipefail
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ 未配置 WORKFLOW secret,无法 fetch/push" >&2
exit 1
fi
echo "✅ Token 已配置"
echo "🌿 上游分支: ${{ env.SOURCE_BRANCH }}"
echo "📝 上游提交: ${{ env.SOURCE_SHA }}"
- name: 📥 对齐 Worktree
id: prepare_worktree
shell: bash
run: |
set -euo pipefail
echo "========================================"
echo "📦 Prepare repo in WORKSPACE_DIR"
echo "========================================"
REPO_NAME="${{ github.event.repository.name }}"
TOKEN="${{ secrets.WORKFLOW }}"
mkdir -p "${{ env.WORKSPACE_DIR }}"
REPO_DIR="$(mktemp -d "${{ env.WORKSPACE_DIR }}/${REPO_NAME}.XXXXXX")"
if [ -n "$TOKEN" ]; then
REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git"
else
REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git"
HEAD_SHA="${{ env.SOURCE_SHA }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/${WORKSPACE_SLOT}"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
if [ ! -d "$REPOSITORY_DIR" ] || \
[ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then
echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2
exit 1
fi
git clone "$REPO_URL" "$REPO_DIR"
# 在共享仓库上配置读取 ACCESS_TOKEN 的凭证助手,供 worktree 内后续
# fetch/push 透明鉴权。token 不落盘,助手在 git 调用时从环境读取。
# 幂等:同一 runner 复用共享仓库时,credential.helper 已是多值(空值 +
# helper),单值写入会报 "cannot overwrite multiple values";故先 unset
# 再用两个 --add 重建(空值清空继承链,再追加读环境的助手)。
git --git-dir="$REPOSITORY_DIR" config credential.interactive never
git --git-dir="$REPOSITORY_DIR" config --unset-all credential.helper 2>/dev/null || true
git --git-dir="$REPOSITORY_DIR" config --add credential.helper ''
git --git-dir="$REPOSITORY_DIR" config --add credential.helper '!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f'
# 固化 origin 到无 token 的 HTTPS URL:区域脚本内保留的历史
# `git remote set-url origin https://oauth2:$TOKEN@...` 仅在设置了
# WORKFLOW env 时才触发;本 job 严禁设置 WORKFLOW,否则 token 会落盘到
# 共享 bare 仓库的 config 并污染所有其它 slot。此处显式重置以固化约束。
git --git-dir="$REPOSITORY_DIR" remote set-url origin "${{ github.server_url }}/${{ github.repository }}.git"
git -C "$REPO_DIR" fetch origin main
git -C "$REPO_DIR" checkout -B main origin/main
if [ ! -f "$WORKTREE_DIR/.git" ]; then
exec 9>"$WORKTREE_LOCK"
flock 9
git config --global --add safe.directory "$REPO_DIR"
echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
echo "✅ Repo prepared"
if [ ! -f "$WORKTREE_DIR/.git" ]; then
if [ -e "$WORKTREE_DIR" ]; then
echo "Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2
exit 1
fi
mkdir -p "$(dirname "$WORKTREE_DIR")"
git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA"
fi
- name: ♻️ Update thirdparty and sync main
flock -u 9
fi
common_dir=$(git -C "$WORKTREE_DIR" rev-parse --path-format=absolute --git-common-dir)
if [ "$common_dir" != "$(realpath "$REPOSITORY_DIR")" ]; then
echo "Worktree 不属于共享仓库: $WORKTREE_DIR" >&2
exit 1
fi
git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA"
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
actual_sha=$(git -C "$WORKTREE_DIR" rev-parse HEAD)
if [ "$actual_sha" != "$HEAD_SHA" ]; then
echo "Worktree 提交不匹配: 期望 $HEAD_SHA,实际 $actual_sha" >&2
exit 1
fi
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV"
echo "Worktree: $WORKTREE_DIR @ $actual_sha"
- name: 📥 更新快照并同步 main
shell: bash
run: |
set -euo pipefail
@@ -562,7 +633,12 @@ jobs:
echo "✅ Update and sync finished."
- name: 🧹 Clean temporary repo
- name: 🧹 重置 Worktree
if: always()
shell: bash
run: |
rm -rf "$REPO_DIR"
# 持久化 worktree 由下次运行前的 detached checkout + reset + clean 复用,
# 此处仅在结束时 detach,避免留下已检出的分支阻塞其它 worktree。
if [ -n "${REPO_DIR:-}" ] && [ -f "$REPO_DIR/.git" ]; then
git -C "$REPO_DIR" checkout --detach --force "${SOURCE_SHA}" 2>/dev/null || true
fi
+6 -6
View File
@@ -31,13 +31,13 @@ python test/integration/check_doc_links.py
## 🧭 CI 自动化测试
测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/test.yml`):
测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/checks.yml``tests` job):
- **触发时机**
- 推送到 `main` 分支
- Pull Request 到 `main` 分支
- 手动触发(workflow_dispatch
- **运行平台**ubuntu-22.04
- **触发方式**`workflow_run`——「🧰 准备环境」(`prepare.yml`)完成后触发。
Prepare 监听 `push:main``pull_request``workflow_dispatch` 与每日 `schedule`
故推送到 main、对 main 开 PR 或手动运行 Prepare 都会经 Prepare 传导到测试。
- **运行平台**standard-ubuntu-22
- **注**`checks.yml` 同时包含 commit message 规范校验(`standards` job)。
## 📚 测试详解