♻️ refactor(ci): 重构为 Prepare 上游 + workflow_run 下游消费者模型

- 新增 prepare.yml:统一检查/安装公共工具并集中 fetch 到共享 bare 仓库
- 统计与发布改为消费成功的 Prepare workflow_run,各自维护 stats/release worktree
- 四个 workflow name 统一为带 emoji 的中文,同步 workflow_run 触发引用
- 系统信息 workflow 保持独立直接触发
- 删除 tests/ 手动验证脚本
This commit is contained in:
csh
2026-07-20 16:21:06 +08:00
parent 610c2b3e74
commit 50efc4bbfa
10 changed files with 528 additions and 977 deletions
+185 -95
View File
@@ -1,9 +1,10 @@
name: 📦 自动发布
on:
push:
tags:
- "[0-9]*"
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: release-${{ github.repository }}
@@ -16,7 +17,7 @@ env:
# ===== Token 配置 =====
# Gitea/GitHub 访问令牌
# 用途:
# 1. Git 操作:克隆仓库、推送提交
# 1. Git 操作:推送提交(仓库读取由 Prepare 完成)
# 2. API 调用:创建 Release、上传附件
# 权限要求:
# - repo (仓库完整访问权限)
@@ -26,7 +27,7 @@ env:
#
# 💡 提示:同一个 token 可用于所有操作
# 如需分离权限,可配置两个不同的 token:
# - GIT_TOKEN: 用于 git clone/push
# - GIT_TOKEN: 用于 git push
# - API_TOKEN: 用于 API 调用
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
GIT_CONFIG_COUNT: "3"
@@ -38,7 +39,7 @@ env:
GIT_CONFIG_VALUE_2: "never"
# ===== 工作区配置 =====
WORKSPACE_DIR: "/data/workspace"
WORKSPACE_ROOT: "/data/workspace"
WORKSPACE_SLOT: "release"
# ===== 分支配置 =====
@@ -134,10 +135,100 @@ env:
# 4. CHANGELOG内容是叠加而非覆盖
jobs:
route-release:
if: ${{ github.event.workflow_run.conclusion == 'success' && contains(github.event.workflow_run.path, '@refs/tags/') }}
runs-on: standard-ubuntu-22
permissions:
contents: read
outputs:
should_release: ${{ steps.route.outputs.should_release }}
version: ${{ steps.route.outputs.version }}
tag_ref: ${{ steps.route.outputs.tag_ref }}
source_sha: ${{ steps.route.outputs.source_sha }}
env:
UPSTREAM_PATH: ${{ github.event.workflow_run.path }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
steps:
- name: 🧭 恢复并验证 Tag
id: route
shell: bash
run: |
set -euo pipefail
if ! command -v git >/dev/null 2>&1; then
echo "Prepare 成功后仍缺少发布路由所需命令: git" >&2
exit 1
fi
echo "should_release=false" >> "$GITHUB_OUTPUT"
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
FULL_REF="${UPSTREAM_PATH##*@}"
case "$FULL_REF" in
refs/tags/*) ;;
*)
echo "workflow_run.path 未包含有效 Tag ref: $UPSTREAM_PATH" >&2
exit 1
;;
esac
VERSION="${FULL_REF#refs/tags/}"
case "$VERSION" in
[0-9]*) ;;
*)
echo "跳过非版本 Tag: $VERSION"
exit 0
;;
esac
if ! git check-ref-format "$FULL_REF"; then
echo "数字开头的 Tag ref 格式无效: $FULL_REF" >&2
exit 1
fi
REPO_NAME="${{ github.event.repository.name }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
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
if ! tag_sha=$(git --git-dir="$REPOSITORY_DIR" rev-parse "${FULL_REF}^{commit}" 2>/dev/null); then
echo "共享仓库中不存在可解析的 Tag: $FULL_REF" >&2
exit 1
fi
if [ "$tag_sha" != "$HEAD_SHA" ]; then
echo "Tag/SHA 不匹配: $FULL_REF -> $tag_sha,上游为 $HEAD_SHA" >&2
exit 1
fi
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag_ref=$FULL_REF" >> "$GITHUB_OUTPUT"
echo "source_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "发布路由通过: $VERSION @ $HEAD_SHA"
changelog-and-release:
runs-on: ubuntu-22.04
needs: route-release
if: ${{ needs.route-release.outputs.should_release == 'true' }}
runs-on: standard-ubuntu-22
permissions:
contents: write
env:
VERSION: ${{ needs.route-release.outputs.version }}
TAG_REF: ${{ needs.route-release.outputs.tag_ref }}
SOURCE_SHA: ${{ needs.route-release.outputs.source_sha }}
steps:
- name: ⚙️ 加载配置
@@ -148,7 +239,7 @@ jobs:
echo "======================================"
# 从环境变量读取配置
VERSION="${{ github.ref_name }}"
VERSION="${{ env.VERSION }}"
KEYWORDS="${{ env.PRERELEASE_KEYWORDS }}"
STRIP_REGEX="-($KEYWORDS)[0-9]*$" # 用于 sed 删除后缀
@@ -235,78 +326,91 @@ jobs:
echo "======================================"
echo ""
- name: 📥 准备隔离仓库
id: clone
- name: 📥 对齐发布 Worktree
id: prepare_worktree
shell: bash
run: |
set -euo pipefail
echo "======================================"
echo "🚀 开始准备仓库"
echo "======================================"
REPO_SLUG="${{ github.repository }}"
for command in git python3 curl flock realpath; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Prepare 成功后仍缺少发布所需命令: $command" >&2
exit 1
fi
done
REPO_NAME="${{ github.event.repository.name }}"
HEAD_SHA="${{ env.SOURCE_SHA }}"
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
WORKSPACE_DIR="${{ env.WORKSPACE_DIR }}"
WORKSPACE_SLOT="${{ env.WORKSPACE_SLOT }}"
REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"
REMOTE_URL="${GITHUB_SERVER_URL}/${REPO_SLUG}.git"
REMOTE_REF="refs/remotes/origin/${MAIN_BRANCH}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/release"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
MAIN_REMOTE_REF="refs/remotes/origin/${MAIN_BRANCH}"
if [ -z "$MAIN_BRANCH" ]; then
echo "❌ 错误: 未配置主分支"
exit 1
fi
case "$REMOTE_URL" in
https://*) ;;
*)
echo "❌ 拒绝通过非 HTTPS remote 使用 WORKFLOW token" >&2
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的路由提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
if [ -e "$REPO_DIR" ] && [ ! -d "$REPO_DIR/.git" ]; then
echo "❌ Slot 已存在但不是 Git 仓库: $REPO_DIR" >&2
if [ -z "$MAIN_BRANCH" ]; then
echo "未配置 MAIN_BRANCH" >&2
exit 1
fi
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
if ! git --git-dir="$REPOSITORY_DIR" show-ref --verify --quiet "$MAIN_REMOTE_REF"; then
echo "Prepare 未 fetch 配置的主分支: $MAIN_REMOTE_REF" >&2
exit 1
fi
echo "📁 仓库名称: $REPO_NAME"
echo "📦 工作区根目录: $WORKSPACE_DIR"
echo "🎯 Workflow 职责: $WORKSPACE_SLOT"
echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo ""
if [ ! -f "$WORKTREE_DIR/.git" ]; then
exec 9>"$WORKTREE_LOCK"
flock 9
if [ ! -d "$REPO_DIR/.git" ]; then
mkdir -p "$(dirname "$REPO_DIR")"
git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"
else
git -C "$REPO_DIR" remote set-url origin "$REMOTE_URL"
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
flock -u 9
fi
git -C "$REPO_DIR" fetch \
--prune --prune-tags --tags --force origin \
'+refs/heads/*:refs/remotes/origin/*'
if ! git -C "$REPO_DIR" show-ref --verify --quiet "$REMOTE_REF"; then
echo "❌ 配置的远程分支不存在: $MAIN_BRANCH" >&2
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 "$REPO_DIR" checkout --force -B "$MAIN_BRANCH" "$REMOTE_REF"
git -C "$REPO_DIR" reset --hard "$REMOTE_REF"
git -C "$REPO_DIR" clean -ffdx
echo "✓ 仓库目录: $REPO_DIR"
echo ""
git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA"
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
cd "$REPO_DIR"
echo "✓ 主分支 '$MAIN_BRANCH' 已准备并清理"
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 "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
git -C "$WORKTREE_DIR" config user.name "${{ env.GIT_USER_NAME }}"
git -C "$WORKTREE_DIR" config user.email "${{ env.GIT_USER_EMAIL }}"
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV"
echo "WORKSPACE_SLOT=$WORKSPACE_SLOT" >> "$GITHUB_ENV"
echo "MAIN_BRANCH=$MAIN_BRANCH" >> "$GITHUB_ENV"
echo ""
echo "✅ 仓库准备完成"
echo "======================================"
echo ""
echo "MAIN_REMOTE_REF=$MAIN_REMOTE_REF" >> "$GITHUB_ENV"
echo "发布 Worktree: $WORKTREE_DIR @ $actual_sha"
- name: 🤖 检查 Bot 提交
id: check_bot
@@ -317,7 +421,7 @@ jobs:
cd ${{ env.REPO_DIR }}
COMMIT_MSG=$(git log -1 --pretty=%B ${{ github.ref_name }})
COMMIT_MSG=$(git log -1 --pretty=%B "${{ env.SOURCE_SHA }}")
echo "📝 Tag commit message:"
echo "$COMMIT_MSG"
echo ""
@@ -344,7 +448,7 @@ jobs:
cd ${{ env.REPO_DIR }}
VERSION="${{ github.ref_name }}"
VERSION="${{ env.VERSION }}"
CHANGELOG_VERSION="${{ env.CHANGELOG_VERSION }}"
echo "🏷️ Tag: $VERSION"
@@ -607,8 +711,12 @@ jobs:
echo ""
echo "💾 写入 CHANGELOG.md..."
# 切换到主分支
git checkout ${{ env.MAIN_BRANCH }}
# 从 Prepare 已 fetch 的远端引用准备主分支,再写入 CHANGELOG
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
MAIN_REMOTE_REF="${{ env.MAIN_REMOTE_REF }}"
git -C "${{ env.REPO_DIR }}" checkout --force -B "$MAIN_BRANCH" "$MAIN_REMOTE_REF"
git -C "${{ env.REPO_DIR }}" reset --hard "$MAIN_REMOTE_REF"
git -C "${{ env.REPO_DIR }}" clean -ffdx
# ============================================
# 更新或创建 CHANGELOG.md
@@ -819,9 +927,6 @@ jobs:
cd ${{ env.REPO_DIR }}
# 确保使用最新的 CHANGELOG
git -C "${{ env.REPO_DIR }}" pull --ff-only origin "${{ env.MAIN_BRANCH }}"
python3 << 'PYSCRIPT'
import re
@@ -871,41 +976,26 @@ jobs:
echo "📄 Draft: ${{ env.RELEASE_IS_DRAFT }}"
echo ""
# 转换 bash 布尔值为 Python 布尔值
if [ "${{ env.RELEASE_IS_DRAFT }}" = "true" ]; then
PY_DRAFT="True"
else
PY_DRAFT="False"
fi
if [ "${{ env.RELEASE_IS_PRERELEASE }}" = "true" ]; then
PY_PRERELEASE="True"
else
PY_PRERELEASE="False"
fi
echo "🔄 转换布尔值: draft=${{ env.RELEASE_IS_DRAFT }} → $PY_DRAFT, prerelease=${{ env.RELEASE_IS_PRERELEASE }} → $PY_PRERELEASE"
echo ""
python3 << EOF
python3 << 'PYSCRIPT'
import json
import os
with open('/tmp/release-body.txt', 'r') as f:
body = f.read()
with open('/tmp/release-body.txt', 'r', encoding='utf-8') as release_body_file:
body = release_body_file.read()
payload = {
"tag_name": "${{ github.ref_name }}",
"name": "${{ env.RELEASE_TITLE_PROCESSED }}",
"tag_name": os.environ["VERSION"],
"name": os.environ["RELEASE_TITLE_PROCESSED"],
"body": body,
"draft": $PY_DRAFT,
"prerelease": $PY_PRERELEASE
"draft": os.environ["RELEASE_IS_DRAFT"] == "true",
"prerelease": os.environ["RELEASE_IS_PRERELEASE"] == "true",
}
with open('/tmp/payload.json', 'w', encoding='utf-8') as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
with open('/tmp/payload.json', 'w', encoding='utf-8') as payload_file:
json.dump(payload, payload_file, ensure_ascii=False, indent=2)
print("✓ Payload prepared")
EOF
PYSCRIPT
echo "🌐 发送 API 请求..."
HTTP_CODE=$(curl -s -o /tmp/release_response.json -w "%{http_code}" -X POST \
@@ -1010,7 +1100,7 @@ jobs:
| 项目 | 结果 |
|------|------|
| 🏷️ Tag 版本 | **`${{ github.ref_name }}`** |
| 🏷️ Tag 版本 | **`${{ env.VERSION }}`** |
| 📝 CHANGELOG 版本 | **`${{ env.CHANGELOG_VERSION }}`** |
| 🌿 主分支 | **`${{ env.MAIN_BRANCH }}`** |
| 🌐 Gitea 服务器 | **`${{ env.GITEA_SERVER }}`** |
@@ -1113,7 +1203,7 @@ jobs:
echo " 原因: 检测到 [skip ci] 标记,防止无限循环"
elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then
echo "📊 执行结果:"
echo " - Tag: ${{ github.ref_name }}"
echo " - Tag: ${{ env.VERSION }}"
echo " - CHANGELOG 版本: ${{ env.CHANGELOG_VERSION }}"
echo " - 主分支: ${{ env.MAIN_BRANCH }}"
echo " - Release 标题: ${{ env.RELEASE_TITLE_PROCESSED }}"
+176
View File
@@ -0,0 +1,176 @@
name: 🧰 准备环境
on:
push:
workflow_dispatch:
concurrency:
group: prepare-${{ github.repository }}
cancel-in-progress: false
env:
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
GIT_CONFIG_COUNT: "3"
GIT_CONFIG_KEY_0: "credential.helper"
GIT_CONFIG_VALUE_0: ""
GIT_CONFIG_KEY_1: '!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f'
GIT_CONFIG_KEY_2: "credential.interactive"
GIT_CONFIG_VALUE_2: "never"
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"
[jq]="jq"
[bc]="bc"
[flock]="util-linux"
[realpath]="coreutils"
)
required_commands=(git python3 curl jq bc 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"
HEAD_SHA="${{ github.sha }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
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 --git-dir="$REPOSITORY_DIR" fetch \
--prune --prune-tags --tags --force origin \
'+refs/heads/*:refs/remotes/origin/*'
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在触发提交: $HEAD_SHA" >&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"
+24 -24
View File
@@ -1,4 +1,4 @@
name: Ubuntu System Information
name: 🖥️ Ubuntu 系统信息
on:
push:
@@ -18,75 +18,75 @@ jobs:
echo "====================================="
echo ""
- name: 🖥 显示系统信息
- name: 显示系统信息
run: |
echo "===== 系统版本 ====="
echo "===== 🐧 系统版本 ====="
cat /etc/os-release
echo ""
echo "===== 内核信息 ====="
echo "===== 🧩 内核信息 ====="
uname -a
echo ""
echo "===== 主机名 ====="
echo "===== 🏷️ 主机名 ====="
hostname
echo ""
echo "===== 系统启动时间 ====="
echo "===== ⏱️ 系统启动时间 ====="
uptime
- name: 🧮 显示 CPU 信息
- name: ⚙️ 显示 CPU 信息
run: |
echo "===== CPU 详细信息 ====="
echo "===== ⚙️ CPU 详细信息 ====="
lscpu
echo ""
echo "===== CPU 型号 ====="
echo "===== 🏷️ CPU 型号 ====="
cat /proc/cpuinfo | grep "model name" | head -1
echo ""
echo "===== CPU 核心数 ====="
echo "===== 🔢 CPU 核心数 ====="
nproc
- name: 🧠 显示内存信息
run: |
echo "===== 内存使用情况 ====="
echo "===== 📊 内存使用情况 ====="
free -h
echo ""
echo "===== 内存详细信息 ====="
echo "===== 🧠 内存详细信息 ====="
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable'
- name: 💾 显示磁盘信息
run: |
echo "===== 磁盘使用情况 ====="
echo "===== 📊 磁盘使用情况 ====="
df -h
echo ""
echo "===== 磁盘分区信息 ====="
echo "===== 🗂️ 磁盘分区信息 ====="
lsblk
- name: 🌐 显示网络信息
run: |
echo "===== 网络接口 ====="
echo "===== 🔌 网络接口 ====="
cat /proc/net/dev
echo ""
echo "===== 路由表 ====="
echo "===== 🧭 路由表 ====="
cat /proc/net/route
echo ""
echo "===== DNS 配置 ====="
echo "===== 📡 DNS 配置 ====="
cat /etc/resolv.conf
- name: 📦 显示已安装的软件
run: |
echo "===== Python 版本 ====="
echo "===== 🐍 Python 版本 ====="
python3 --version
echo ""
echo "===== Node.js 版本 ====="
echo "===== 🟢 Node.js 版本 ====="
node --version || echo "Node.js 未安装"
echo ""
echo "===== Docker 版本 ====="
echo "===== 🐳 Docker 版本 ====="
docker --version || echo "Docker 未安装"
echo ""
echo "===== Git 版本 ====="
echo "===== 🔀 Git 版本 ====="
git --version
- name: 🔧 显示环境变量
run: |
echo "===== 重要环境变量 ====="
echo "===== 🔧 重要环境变量 ====="
echo "PATH: $PATH"
echo "HOME: $HOME"
echo "USER: $USER"
@@ -95,8 +95,8 @@ jobs:
- name: 📂 显示当前目录
run: |
echo "===== 当前工作目录 ====="
echo "===== 📍 当前工作目录 ====="
pwd
echo ""
echo "===== 目录内容 ====="
echo "===== 📄 目录内容 ====="
ls -lah
@@ -1,9 +1,10 @@
name: 📊 代码统计徽章
on:
push:
branches: [main, master]
workflow_dispatch:
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: stats-${{ github.repository }}
@@ -25,8 +26,10 @@ env:
GIT_CONFIG_VALUE_2: "never"
# ===== 工作区配置 =====
WORKSPACE_DIR: "/data/workspace"
WORKSPACE_ROOT: "/data/workspace"
WORKSPACE_SLOT: "stats"
SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }}
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
# ===== 分支配置 =====
# 徽章数据存储分支(可配置)
@@ -78,7 +81,8 @@ env:
jobs:
update-stats:
runs-on: ubuntu-22.04
if: ${{ github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') }}
runs-on: standard-ubuntu-22
permissions:
contents: write
@@ -98,195 +102,105 @@ jobs:
echo "token_type=WORKFLOW" >> $GITHUB_OUTPUT
echo "🔗 仓库: ${{ github.repository }}"
echo "🌿 分支: ${{ github.ref_name }}"
echo "📝 提交: ${GITHUB_SHA:0:7}"
echo "🌿 上游分支: ${{ env.SOURCE_BRANCH }}"
echo "📝 上游提交: ${{ env.SOURCE_SHA }}"
- name: 🔧 检查必需工具
id: check_deps
run: |
echo "======================================"
echo "🔍 检查依赖工具"
echo "======================================"
MISSING_TOOLS=()
# 检查 bc(用于数字计算)
if ! command -v bc &> /dev/null; then
echo "⚠️ bc 未安装"
MISSING_TOOLS+=("bc")
else
echo "✓ bc 已安装"
fi
# 检查 jq(用于 JSON 处理)
if ! command -v jq &> /dev/null; then
echo "⚠️ jq 未安装"
MISSING_TOOLS+=("jq")
else
echo "✓ jq 已安装"
fi
# 如果有缺失的工具,安装它们
if [ ${#MISSING_TOOLS[@]} -gt 0 ]; then
echo ""
echo "📦 安装缺失的工具: ${MISSING_TOOLS[*]}"
# 🔧 智能判断是否需要 sudo
# Docker 环境通常以 root 运行,不需要 sudo
if [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then
# 当前是 root 用户,直接执行
echo "🔑 检测到 root 权限,直接安装"
apt-get update -qq
apt-get install -y -qq "${MISSING_TOOLS[@]}"
elif command -v sudo &> /dev/null; then
# 有 sudo 命令,使用 sudo
echo "🔑 使用 sudo 安装"
sudo apt-get update -qq
sudo apt-get install -y -qq "${MISSING_TOOLS[@]}"
else
# 既不是 root 也没有 sudo
echo "❌ 错误:没有足够权限安装工具"
echo "请在 Docker 镜像中预装以下工具:"
echo " ${MISSING_TOOLS[*]}"
echo ""
echo "方案1: 使用包含这些工具的镜像"
echo "方案2: 在 Dockerfile 中添加:"
echo " RUN apt-get update && apt-get install -y bc jq"
exit 1
fi
echo "installed=true" >> $GITHUB_OUTPUT
echo "✅ 工具安装完成"
else
echo "installed=false" >> $GITHUB_OUTPUT
echo "✅ 所有工具已就绪"
fi
echo "======================================"
echo ""
- name: 📥 准备隔离仓库
id: clone_main
- name: 📥 对齐统计 Worktree
id: prepare_worktree
shell: bash
run: |
set -euo pipefail
echo "======================================"
echo "🚀 开始准备主仓库"
echo "======================================"
REPO_SLUG="${{ github.repository }}"
for command in git python3 bc flock realpath; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Prepare 成功后仍缺少统计所需命令: $command" >&2
exit 1
fi
done
REPO_NAME="${{ github.event.repository.name }}"
TARGET_BRANCH="${{ github.ref_name }}"
WORKSPACE_DIR="${{ env.WORKSPACE_DIR }}"
WORKSPACE_SLOT="${{ env.WORKSPACE_SLOT }}"
REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"
REMOTE_URL="${GITHUB_SERVER_URL}/${REPO_SLUG}.git"
REMOTE_REF="refs/remotes/origin/${TARGET_BRANCH}"
HEAD_SHA="${{ env.SOURCE_SHA }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/stats"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
case "$REMOTE_URL" in
https://*) ;;
*)
echo "❌ 拒绝通过非 HTTPS remote 使用 WORKFLOW token" >&2
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
if [ -e "$REPO_DIR" ] && [ ! -d "$REPO_DIR/.git" ]; then
echo "❌ Slot 已存在但不是 Git 仓库: $REPO_DIR" >&2
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
echo "📁 仓库名称: $REPO_NAME"
echo "📦 工作区根目录: $WORKSPACE_DIR"
echo "🎯 Workflow 职责: $WORKSPACE_SLOT"
echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo "🌿 分支: $TARGET_BRANCH"
echo ""
if [ ! -f "$WORKTREE_DIR/.git" ]; then
exec 9>"$WORKTREE_LOCK"
flock 9
if [ ! -d "$REPO_DIR/.git" ]; then
mkdir -p "$(dirname "$REPO_DIR")"
git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"
else
git -C "$REPO_DIR" remote set-url origin "$REMOTE_URL"
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
flock -u 9
fi
git -C "$REPO_DIR" fetch \
--prune --prune-tags --tags --force origin \
'+refs/heads/*:refs/remotes/origin/*'
if ! git -C "$REPO_DIR" show-ref --verify --quiet "$REMOTE_REF"; then
echo "❌ 触发分支不存在: $TARGET_BRANCH" >&2
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 "$REPO_DIR" checkout --force -B "$TARGET_BRANCH" "$REMOTE_REF"
git -C "$REPO_DIR" reset --hard "$REMOTE_REF"
git -C "$REPO_DIR" clean -ffdx
echo "✓ 仓库目录: $REPO_DIR"
echo ""
git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA"
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
cd "$REPO_DIR"
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 "✅ 仓库准备成功"
echo ""
git -C "$WORKTREE_DIR" config user.name "${{ env.GIT_USER_NAME }}"
git -C "$WORKTREE_DIR" config user.email "${{ env.GIT_USER_EMAIL }}"
# 配置 Git 用户信息
echo "⚙️ 配置 Git 用户信息..."
git config user.name "${{ env.GIT_USER_NAME }}"
git config user.email "${{ env.GIT_USER_EMAIL }}"
# 显示仓库信息
echo "📊 仓库信息:"
echo " - 当前分支: $(git branch --show-current)"
echo " - 最新提交: $(git log -1 --oneline)"
echo " - 远程地址: $(git remote get-url origin | sed 's/oauth2:.*@/oauth2:***@/')"
echo ""
# 导出环境变量
echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV"
echo "WORKSPACE_SLOT=$WORKSPACE_SLOT" >> "$GITHUB_ENV"
echo "✅ 主仓库准备完成"
echo "======================================"
echo ""
echo "统计 Worktree: $WORKTREE_DIR @ $actual_sha"
- name: 🌿 准备统计分支
id: prepare_stats_branch
run: |
echo "======================================"
echo "🌿 准备统计分支"
echo "======================================"
set -euo pipefail
cd "${{ env.REPO_DIR }}"
REPOSITORY_DIR="${{ env.REPOSITORY_DIR }}"
WORKTREE_DIR="${{ env.REPO_DIR }}"
BADGE_BRANCH="${{ env.BADGE_BRANCH }}"
SOURCE_SHA="${{ env.SOURCE_SHA }}"
# 获取所有远程分支
git -C "${{ env.REPO_DIR }}" fetch origin --prune
# 检查统计分支是否存在
if git -C "${{ env.REPO_DIR }}" ls-remote --heads origin "${{ env.BADGE_BRANCH }}" | \
grep -q "refs/heads/${{ env.BADGE_BRANCH }}"; then
echo "✅ 统计分支 '${{ env.BADGE_BRANCH }}' 已存在"
# 检出统计分支
git -C "${{ env.REPO_DIR }}" fetch origin \
"+refs/heads/${{ env.BADGE_BRANCH }}:refs/remotes/origin/${{ env.BADGE_BRANCH }}"
git checkout --force -B "${{ env.BADGE_BRANCH }}" "refs/remotes/origin/${{ env.BADGE_BRANCH }}"
echo "📂 当前分支内容:"
ls -la
# 返回主分支
git checkout ${{ github.ref_name }}
echo "branch_exists=true" >> $GITHUB_OUTPUT
if git --git-dir="$REPOSITORY_DIR" show-ref --verify --quiet "refs/remotes/origin/${BADGE_BRANCH}"; then
git -C "$WORKTREE_DIR" checkout --force -B "$BADGE_BRANCH" "refs/remotes/origin/${BADGE_BRANCH}"
echo "branch_exists=true" >> "$GITHUB_OUTPUT"
else
echo "🆕 统计分支 '${{ env.BADGE_BRANCH }}' 不存在,将创建"
if git show-ref --verify --quiet "refs/heads/${{ env.BADGE_BRANCH }}"; then
echo "🧹 删除上次失败运行遗留的本地统计分支"
git branch -D "${{ env.BADGE_BRANCH }}"
if git --git-dir="$REPOSITORY_DIR" show-ref --verify --quiet "refs/heads/${BADGE_BRANCH}"; then
git -C "$WORKTREE_DIR" branch -D "$BADGE_BRANCH"
fi
echo "branch_exists=false" >> $GITHUB_OUTPUT
echo "branch_exists=false" >> "$GITHUB_OUTPUT"
fi
echo "======================================"
echo ""
git -C "$WORKTREE_DIR" checkout --detach --force "$SOURCE_SHA"
- name: 🌱 创建统计分支
if: steps.prepare_stats_branch.outputs.branch_exists == 'false'
@@ -296,26 +210,12 @@ jobs:
echo "======================================"
cd "${{ env.REPO_DIR }}"
# 🔴 安全检查:确认在正确的目录
echo "🔍 当前目录: $(pwd)"
echo "🔍 .git 状态: $([ -d .git ] && echo '✓ 存在' || echo '✗ 不存在')"
if [ ! -d .git ]; then
echo "❌ 错误:.git 目录不存在,无法创建分支"
exit 1
fi
git rev-parse --is-inside-work-tree >/dev/null
# 创建孤立分支(orphan branch
# 📝 说明:孤立分支与主分支没有共同的提交历史,适合存储独立的数据
echo "🌱 创建孤立分支..."
git checkout --orphan ${{ env.BADGE_BRANCH }}
# 🔴 再次确认 .git 仍然存在
if [ ! -d .git ]; then
echo "❌ 严重错误:.git 目录丢失!"
exit 1
fi
git checkout --orphan "${{ env.BADGE_BRANCH }}"
# 🧹 清空所有文件(但保留 .git 目录)
# ❓ 为什么要删除?
@@ -336,16 +236,12 @@ jobs:
# 步骤 2: 安全地删除剩余文件
# 使用更严格的保护,确保不会误删 .git
echo "🔍 查找需要删除的文件..."
find . -maxdepth 1 -type f ! -name '.gitignore' -delete 2>/dev/null || true
find . -maxdepth 1 -type d ! -name '.' ! -name '..' ! -name '.git' ! -name '.gitea' -exec rm -rf {} + 2>/dev/null || true
find . -maxdepth 1 -type f ! -name '.git' ! -name '.gitignore' -delete 2>/dev/null || true
find . -maxdepth 1 -type d ! -name '.' ! -name '..' ! -name '.gitea' -exec rm -rf {} + 2>/dev/null || true
# 🔴 最终检查:确认 .git 仍然完好
echo "🔍 最终检查..."
if [ ! -d .git ]; then
echo "❌ 致命错误:.git 目录在清理过程中被删除!"
echo "这不应该发生,请检查 find 命令"
exit 1
fi
git rev-parse --is-inside-work-tree >/dev/null
echo "✓ .git 目录完好"
echo "✓ 文件清理完成"
echo ""
@@ -371,8 +267,8 @@ jobs:
## 🔄 更新机制
- 每次 push 到主分支时自动更新
- ✅ 可通过 workflow_dispatch 手动触发
- 主分支的 Prepare 成功后自动更新
- ✅ 可在 main/master 上手动运行 Prepare
## 📊 数据来源
@@ -384,7 +280,7 @@ jobs:
EOF
# 创建徽章目录
mkdir -p ${{ env.BADGE_DIR }}
mkdir -p "${{ env.BADGE_DIR }}"
# 添加并提交
git add .
@@ -397,10 +293,9 @@ jobs:
echo "======================================"
echo ""
# 🔴 重要:返回主分支,后续统计需要在主分支进行
echo "🔄 切换回主分支: ${{ github.ref_name }}"
git checkout ${{ github.ref_name }}
echo "✓ 已切换回主分支"
echo "🔄 返回上游提交: ${{ env.SOURCE_SHA }}"
git checkout --detach --force "${{ env.SOURCE_SHA }}"
echo "✓ 已返回上游提交"
echo ""
- name: 📊 统计总代码量
@@ -412,16 +307,11 @@ jobs:
cd "${{ env.REPO_DIR }}"
# 🔴 重要:确保在主分支进行统计(不要统计 stats 分支)
echo "🔍 确认当前分支..."
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "${{ github.ref_name }}" ]; then
echo "⚠️ 当前在分支: $CURRENT_BRANCH,切换到主分支: ${{ github.ref_name }}"
git checkout ${{ github.ref_name }}
else
echo "✓ 已在主分支: $CURRENT_BRANCH"
ACTUAL_SHA=$(git rev-parse HEAD)
if [ "$ACTUAL_SHA" != "${{ env.SOURCE_SHA }}" ]; then
echo "源码 Worktree 不在上游提交: 期望 ${{ env.SOURCE_SHA }},实际 $ACTUAL_SHA" >&2
exit 1
fi
echo ""
# 构建排除参数
EXCLUDE_FIND_ARGS=()
@@ -465,16 +355,11 @@ jobs:
cd "${{ env.REPO_DIR }}"
# 🔴 重要:确保在主分支进行统计
echo "🔍 确认当前分支..."
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "${{ github.ref_name }}" ]; then
echo "⚠️ 当前在分支: $CURRENT_BRANCH,切换到主分支: ${{ github.ref_name }}"
git checkout ${{ github.ref_name }}
else
echo "✓ 已在主分支: $CURRENT_BRANCH"
ACTUAL_SHA=$(git rev-parse HEAD)
if [ "$ACTUAL_SHA" != "${{ env.SOURCE_SHA }}" ]; then
echo "源码 Worktree 不在上游提交: 期望 ${{ env.SOURCE_SHA }},实际 $ACTUAL_SHA" >&2
exit 1
fi
echo ""
# 创建临时目录
mkdir -p /tmp/lang_stats
@@ -555,8 +440,8 @@ jobs:
cd "${{ env.REPO_DIR }}"
# 在主分支渲染产物,避免依赖 stats 分支中的脚本文件
git checkout ${{ github.ref_name }}
# 在上游提交渲染产物,避免依赖 stats 分支中的脚本文件
git checkout --detach --force "${{ env.SOURCE_SHA }}"
RENDER_ROOT=$(mktemp -d)
UPDATED_AT=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
@@ -579,7 +464,7 @@ jobs:
--min-lines-threshold "${{ env.MIN_LINES_THRESHOLD }}" \
--lang-summary /tmp/lang_summary.txt
git checkout ${{ env.BADGE_BRANCH }}
git checkout --force "${{ env.BADGE_BRANCH }}"
rm -rf "${{ env.BADGE_DIR }}"
mkdir -p "${{ env.BADGE_DIR }}"
cp -f "$RENDER_ROOT/README.md" README.md
@@ -629,7 +514,7 @@ jobs:
- 语言数: ${{ steps.languages.outputs.language_count }} 种
- 徽章数: ${{ steps.generate_badges.outputs.generated_count }} 个
🔗 触发提交: ${GITHUB_SHA:0:7}
🔗 触发提交: ${{ env.SOURCE_SHA }}
🤖 由 Gitea Actions 自动生成"
git commit -m "$COMMIT_MSG"
@@ -660,7 +545,8 @@ jobs:
| 项目 | 结果 |
|------|------|
| 🔐 Token 类型 | **${{ steps.validate_token.outputs.token_type }}** |
| 🔧 依赖检查 | ${{ steps.check_deps.outputs.installed == 'true' && '✅ 已安装缺失工具' || '✅ 所有工具就绪' }} |
| 🌿 来源分支 | `${{ env.SOURCE_BRANCH }}` |
| 📝 来源提交 | `${{ env.SOURCE_SHA }}` |
| 📊 总代码行数 | **${{ steps.total.outputs.formatted_code }}** 行 |
| 📁 总文件数 | **${{ steps.total.outputs.formatted_files }}** 个 |
| 🌐 语言种类 | **${{ steps.languages.outputs.language_count }}** 种 |
@@ -671,7 +557,7 @@ jobs:
- 📊 [查看详细统计报告](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/README.md)
- 🎨 [浏览徽章文件](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }})
- 🔧 [查看 Workflow 配置](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ github.ref_name }}/.gitea/workflows/update_stats_badge.yaml)
- 🔧 [查看 Workflow 配置](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.SOURCE_BRANCH }}/.gitea/workflows/update_stats_badge.yml)
## 📝 语言分布
@@ -758,12 +644,6 @@ jobs:
echo ""
echo "================================================"
# 如果有警告或特殊情况,也输出
if [ "${{ steps.check_deps.outputs.installed }}" == "true" ]; then
echo ""
echo "ℹ️ 本次运行安装了缺失的依赖工具"
fi
if [ "${{ steps.languages.outputs.language_count }}" == "0" ]; then
echo ""
echo "⚠️ 警告: 未找到符合条件的语言文件"
+6 -2
View File
@@ -39,11 +39,15 @@
- 💡 工作流配置说明
- 🔧 如何使用和定制
- 🔄 案例:`changelog_and_release.yml` - 自动更新 CHANGELOG 和自动创建 Release
- 📊 案例:`update_stats_badge.yaml` - 自动生成代码统计徽章
- 🧰 案例:`prepare.yml` - 检查并补齐公共工具,集中准备共享 bare 仓库
- 📊 案例:`update_stats_badge.yml` - 在成功的 Prepare 主分支运行后生成统计徽章
- 📦 案例:`changelog_and_release.yml` - 在成功的 Prepare 版本 Tag 运行后生成 CHANGELOG 和 Release
- 🖥️ 案例:`ubuntu_system_info.yml` - 直接触发的独立系统信息任务,可与 Prepare 并行
👉 **实用的 Actions 工作流,可直接复制使用**
---
仓库相关工作流采用 `Prepare → workflow_run 消费者` 模型;系统信息不读取仓库,因此保持独立触发。详细的共享目录、并发和恢复规则见 [WORKFLOW.md](./WORKFLOW.md)。
具体部署步骤、预设选择、注册流程、升级方式和故障排查请查看 [DEPLOYMENT.md](./DEPLOYMENT.md)。
+34 -13
View File
@@ -11,23 +11,34 @@
```txt
.gitea/workflows/
├── changelog_and_release.yml # CHANGELOG 生成 + Release 创建
├── update_stats_badge.yaml # 代码行数的统计
── ... # 更多 workflow 待添加
├── prepare.yml # 公共工具 + shared bare repository
├── changelog_and_release.yml # Prepare 成功后的 Tag 发布消费者
── update_stats_badge.yml # Prepare 成功后的主分支统计消费者
└── ubuntu_system_info.yml # 与仓库无关、独立直接触发
```
---
## 🧱 运行模型
本模板中的 workflow 采用“按职责划分固定 slot”的方案:
```text
push / workflow_dispatch
├─ Prepare
│ ├─ 检查并按需安装公共工具
│ └─ fetch → /data/workspace/<repo>/repository.git
└─ Ubuntu System Information(自身触发条件命中时独立并行)
- 每个会写仓库的 workflow 职责拥有一个固定 slotslot 本身就是 Git clone 根目录,不额外包含 `repo` 子目录
- 第一次运行会 clone 仓库,后续运行通过 fetch、`git reset --hard` 和 clean 重置后复用已有 checkout
- 同一 slot 的任务按 concurrency group 串行执行,不同 slot 之间可以并发运行
- 任务结束后保留 slot checkout;长期依赖缓存应放在 checkout 之外,避免被清理命令删除
Prepare completed successfully
├─ main/master → update_stats_badge.yml
└─ refs/tags/[0-9]* → changelog_and_release.yml
```
这个设计适用于仓库和 workflow 职责固定的自托管 CI 服务器,在减少重复 clone 的同时避免多个任务并发修改同一个工作目录
`Prepare` 是仓库相关链路中唯一安装公共工具和 fetch 的 workflow。它保存一个 persistent bare repository,但不创建任何消费者 worktree
统计和发布通过 `workflow_run` 读取上游 `head_sha`;发布还从 Gitea 的 `workflow_run.path`(例如 `prepare.yml@refs/tags/1.2.0`)恢复原始 Tag。两个消费者分别拥有 `worktrees/stats``worktrees/release`,首次创建时通过 `worktree-admin.lock` 串行修改 Git worktree metadata,每次复用前都执行 detached checkout、hard reset 和 clean。
三个仓库相关 workflow 使用各自的 per-repository concurrency group 且不取消排队运行。统计与发布使用不同 worktree,因此可以并行;系统信息 workflow 不依赖仓库或 Prepare,也可以独立并行。
---
@@ -51,6 +62,8 @@
**触发方式**
推送数字开头的 Tag 会先触发 `Prepare`。Prepare 成功后,发布 routing job 从 `workflow_run.path` 恢复 Tag,确认 Tag peeled commit 等于 `head_sha`,随后才生成 CHANGELOG 和 Release。
```bash
git tag 1.0.0
git push origin 1.0.0
@@ -62,7 +75,7 @@ git push origin 1.0.0
---
#### 2. 📊 代码统计徽章工作流 (`update_stats_badge.yaml`)
#### 2. 📊 代码统计徽章工作流 (`update_stats_badge.yml`)
**功能**:自动统计代码行数并生成 SVG 徽章与统计报告
@@ -76,15 +89,17 @@ git push origin 1.0.0
**触发方式**
主分支 push 或在 `main`/`master` 上手动运行 Prepare 后,统计 workflow 通过成功的 `workflow_run` 启动,并始终从上游 `head_sha` 的 detached checkout 读取源码。
```bash
# 推送到主分支时自动触发
git push origin main
# 手动触发
# 仓库 → Actions → Update Code Statistics Badge → Run workflow
# 仓库 → Actions → Prepare → Run workflow
```
**配置文件**[update_stats_badge.yaml](.gitea/workflows/update_stats_badge.yaml)
**配置文件**[update_stats_badge.yml](.gitea/workflows/update_stats_badge.yml)
**markdown引用格式**: `![C++](https://你的gitea/用户名/仓库/raw/branch/stats/badges/cpp-lines.svg)`
@@ -100,7 +115,13 @@ stats
└── <language>-lines.svg
```
💡 **详细配置**:查看 `update_stats_badge.yaml` 文件顶部的 `env` 区域,包含语言分组、颜色、排除目录等配置
💡 **详细配置**:查看 `update_stats_badge.yml` 文件顶部的 `env` 区域,包含语言分组、颜色、排除目录等配置
---
#### 3. 🖥️ 系统信息工作流 (`ubuntu_system_info.yml`)
该 workflow 不读取仓库内容,保留自己的 `push``workflow_dispatch` 触发器;它不使用 `workflow_run``repository.git` 或任何消费者 worktree。
---
-56
View File
@@ -1,56 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
fail() {
echo "FAIL: $*" >&2
exit 1
}
has_crlf() {
local file_path=$1
file "${file_path}" | grep -qi "CRLF\|with CR"
}
test_check_crlf_works_from_preset_directory() {
local temp_root common_dir preset_dir output_file file_name
temp_root=$(mktemp -d)
common_dir="${temp_root}/common"
preset_dir="${temp_root}/preset"
output_file="${temp_root}/output.txt"
mkdir -p "${common_dir}" "${preset_dir}"
cp "${REPO_ROOT}/docker-runner/common/check_crlf.sh" "${common_dir}/check_crlf.sh"
chmod +x "${common_dir}/check_crlf.sh"
for file_name in entrypoint.sh setup.sh upgrade.sh register.sh manage.sh; do
printf '#!/bin/bash\r\necho test\r\n' > "${common_dir}/${file_name}"
chmod 644 "${common_dir}/${file_name}"
done
(
cd "${preset_dir}"
printf 'n\n' | ../common/check_crlf.sh > "${output_file}"
)
! rg -q "文件不存在" "${output_file}" || fail "check_crlf.sh should inspect sibling common scripts even when invoked from preset directory"
! rg -q "是否立即重启容器" "${output_file}" || fail "check_crlf.sh should not prompt to restart containers"
! rg -q "docker compose down" "${output_file}" || fail "check_crlf.sh should not include compose restart commands"
! rg -q "docker compose build --no-cache" "${output_file}" || fail "check_crlf.sh should not take responsibility for rebuilding containers"
! rg -q "docker compose up -d" "${output_file}" || fail "check_crlf.sh should not take responsibility for starting containers"
for file_name in entrypoint.sh setup.sh upgrade.sh register.sh manage.sh; do
! has_crlf "${common_dir}/${file_name}" || fail "${file_name} should have CRLF fixed"
[ -x "${common_dir}/${file_name}" ] || fail "${file_name} should be made executable"
done
rm -rf "${temp_root}"
}
test_check_crlf_works_from_preset_directory
echo "check_crlf_test.sh: PASS"
-62
View File
@@ -1,62 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
fail() {
echo "FAIL: $*" >&2
exit 1
}
temp_root=$(mktemp -d)
lang_summary="${temp_root}/lang_summary.txt"
output_dir="${temp_root}/badges"
readme_path="${temp_root}/README.md"
cat > "${lang_summary}" <<'EOF'
python|Python|1200|1,200|12|3572A5|python
typescript|TypeScript|800|800|9|3178c6|typescript
yaml|YAML|120|120|5|CB171E|
EOF
python3 "${REPO_ROOT}/.gitea/ci/render_stats_svgs.py" \
--output-dir "${output_dir}" \
--readme-path "${readme_path}" \
--repo "csh/actions-template" \
--generated-by "Gitea Actions" \
--updated-at "2026-05-22 12:00:00 UTC" \
--total-code 2120 \
--formatted-total-code "2,120" \
--total-files 26 \
--formatted-total-files "26" \
--language-count 3 \
--exclude-dirs "node_modules,dist,.git" \
--min-lines-threshold 10 \
--lang-summary "${lang_summary}"
test -f "${output_dir}/total-lines.svg" || fail "renderer should create total-lines.svg"
test -f "${output_dir}/total-files.svg" || fail "renderer should create total-files.svg"
test -f "${output_dir}/language-count.svg" || fail "renderer should create language-count.svg"
test -f "${output_dir}/python-lines.svg" || fail "renderer should create per-language svg badges"
test -f "${readme_path}" || fail "renderer should create root README.md"
grep -q '<svg' "${output_dir}/total-lines.svg" || fail "svg badges should contain svg markup"
grep -q '2,120' "${output_dir}/total-lines.svg" || fail "total-lines badge should include formatted total code"
grep -q 'Python' "${output_dir}/python-lines.svg" || fail "language badge should include display name"
! grep -q 'fill="#555555"' "${output_dir}/total-lines.svg" || fail "summary badge should not use the heavy gray label background"
grep -q 'fill="#555555"' "${output_dir}/python-lines.svg" || fail "language badge should keep the split label style"
grep -q '\./badges/total-lines.svg' "${readme_path}" || fail "README should reference badges via relative svg paths"
grep -q '\./badges/python-lines.svg' "${readme_path}" || fail "README should reference language badges via relative svg paths"
! grep -q '^!\[总代码\](\./badges/total-lines.svg)$' "${readme_path}" || fail "README should not duplicate summary badges above the table"
! grep -q '^!\[总文件\](\./badges/total-files.svg)$' "${readme_path}" || fail "README should not duplicate summary badges above the table"
! grep -q '^!\[语言种类\](\./badges/language-count.svg)$' "${readme_path}" || fail "README should not duplicate summary badges above the table"
grep -q 'Gitea Actions' "${readme_path}" || fail "README should mention Gitea Actions"
grep -q '此分支由 Gitea Actions 自动生成和维护' "${readme_path}" || fail "README should explain that stats branch is generated"
grep -q '请勿手动修改此分支的内容' "${readme_path}" || fail "README should warn against manual edits"
! grep -q 'GitHub Actions' "${readme_path}" || fail "README should not mention GitHub Actions"
! find "${temp_root}" -name '*.json' | grep -q . || fail "renderer should not create json badge payloads"
rm -rf "${temp_root}"
echo "stats_svg_render_test.sh: PASS"
-327
View File
@@ -1,327 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
fail() {
echo "FAIL: $*" >&2
exit 1
}
test_preset_compose_uses_env_for_instance() {
local file
for file in \
"${REPO_ROOT}/docker-runner/presets/standard-ubuntu-22/docker-compose.yml" \
"${REPO_ROOT}/docker-runner/presets/buildx-ubuntu-22/docker-compose.yml" \
"${REPO_ROOT}/docker-runner/presets/buildx-archlinux/docker-compose.yml"; do
grep -q 'GITEA_INSTANCE=${GITEA_INSTANCE}' "${file}" || fail "${file} should read GITEA_INSTANCE from env"
grep -q 'GITEA_TOKEN=${GITEA_TOKEN}' "${file}" || fail "${file} should read GITEA_TOKEN from env"
done
}
test_workflows_do_not_hardcode_company_server() {
! rg -q 'https://git\.mytsl\.cn' "${REPO_ROOT}/.gitea/workflows/changelog_and_release.yml" || fail "changelog workflow should not hardcode company server"
! rg -q 'https://git\.mytsl\.cn' "${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml" || fail "stats workflow should not hardcode company server"
}
test_stats_workflow_uses_workflow_secret_consistently() {
local file
file="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
grep -q 'ACCESS_TOKEN: ${{ secrets.WORKFLOW }}' "${file}" || fail "stats workflow should read ACCESS_TOKEN from WORKFLOW secret"
! rg -q 'STATS_TOKEN' "${file}" || fail "stats workflow should not mention legacy STATS_TOKEN secret"
! rg -q 'GITHUB_TOKEN' "${file}" || fail "stats workflow should not mention GITHUB_TOKEN in token guidance"
}
test_release_workflow_uses_inline_release_slot() {
local file
file="${REPO_ROOT}/.gitea/workflows/changelog_and_release.yml"
grep -Fq 'group: release-${{ github.repository }}' "${file}" || fail "release workflow should serialize its slot per repository"
grep -q '^[[:space:]]*cancel-in-progress: false$' "${file}" || fail "release slot should queue instead of canceling"
grep -Fq 'WORKSPACE_DIR: "/data/workspace"' "${file}" || fail "release workflow should use /data/workspace"
grep -Fq 'WORKSPACE_SLOT: "release"' "${file}" || fail "release workflow should own the release slot"
grep -Fq 'MAIN_BRANCH: "main"' "${file}" || fail "release workflow should keep an explicit MAIN_BRANCH"
grep -Fq '# 必须明确配置主分支(例如 main 或 master' "${file}" || fail "release workflow should document MAIN_BRANCH as required"
! rg -q 'CHANGELOG\.md (已作为附件上传|已上传为附件)' "${file}" || fail "release workflow should not claim CHANGELOG.md was uploaded as a Release asset"
grep -q 'ADDITIONAL_RELEASE_FILES' "${file}" || fail "release workflow should retain optional Release asset configuration"
grep -Fq 'attachment=@${file}' "${file}" || fail "release workflow should retain optional Release asset uploads"
grep -q 'GIT_CONFIG_COUNT: "3"' "${file}" || fail "release workflow should configure transient Git authentication"
grep -Fq 'case "$REMOTE_URL" in' "${file}" || fail "release workflow should reject token use on non-HTTPS remotes"
grep -Fq 'REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"' "${file}" || fail "release workflow should build its clone-root slot inline"
grep -Fq 'git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"' "${file}" || fail "release workflow should clone directly into its slot"
grep -Fq 'git -C "$REPO_DIR" checkout --force -B "$MAIN_BRANCH" "$REMOTE_REF"' "${file}" || fail "release workflow should reset its branch from origin"
grep -Fq 'git -C "$REPO_DIR" clean -ffdx' "${file}" || fail "release workflow should clean ignored output before reuse"
grep -Fq 'git -C "${{ env.REPO_DIR }}" push origin "${{ env.MAIN_BRANCH }}"' "${file}" || fail "release workflow should push directly through transient authentication"
! rg -q 'BOOTSTRAP_SCRIPT|prepare-slot-workspace|git-auth|bootstrap_workspace\.sh' "${file}" || fail "release workflow should not use a shared bootstrap helper"
! rg -q 'MIRROR_ROOT|JOB_WORKSPACE_ROOT|MIRROR_PATH|JOB_WORKSPACE|https://oauth2:' "${file}" || fail "release workflow should not use retired paths or credential URLs"
}
test_stats_workflow_uses_inline_stats_slot() {
local file
file="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
grep -Fq 'group: stats-${{ github.repository }}' "${file}" || fail "stats workflow should serialize the stats slot per repository"
grep -q '^[[:space:]]*cancel-in-progress: false$' "${file}" || fail "stats slot should queue instead of canceling"
grep -Fq 'WORKSPACE_DIR: "/data/workspace"' "${file}" || fail "stats workflow should use /data/workspace"
grep -Fq 'WORKSPACE_SLOT: "stats"' "${file}" || fail "stats workflow should own the stats slot"
grep -q 'GIT_CONFIG_COUNT: "3"' "${file}" || fail "stats workflow should configure transient Git authentication"
grep -Fq 'case "$REMOTE_URL" in' "${file}" || fail "stats workflow should reject token use on non-HTTPS remotes"
grep -Fq 'REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"' "${file}" || fail "stats workflow should build its clone-root slot inline"
grep -Fq 'git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"' "${file}" || fail "stats workflow should clone directly into its slot"
grep -Fq 'git -C "$REPO_DIR" checkout --force -B "$TARGET_BRANCH" "$REMOTE_REF"' "${file}" || fail "stats workflow should reset its source branch from origin"
grep -Fq 'git -C "$REPO_DIR" clean -ffdx' "${file}" || fail "stats workflow should clean ignored output before reuse"
grep -Fq 'git checkout --force -B "${{ env.BADGE_BRANCH }}" "refs/remotes/origin/${{ env.BADGE_BRANCH }}"' "${file}" || fail "stats workflow should reset a reused local stats branch to origin"
grep -Fq 'git branch -D "${{ env.BADGE_BRANCH }}"' "${file}" || fail "stats workflow should remove a stale local stats branch when origin is absent"
grep -Fq 'git -C "${{ env.REPO_DIR }}" push origin "${{ env.BADGE_BRANCH }}"' "${file}" || fail "stats workflow should push directly through transient authentication"
! rg -q 'BOOTSTRAP_SCRIPT|prepare-slot-workspace|git-auth|bootstrap_workspace\.sh' "${file}" || fail "stats workflow should not use a shared bootstrap helper"
! rg -q 'MIRROR_ROOT|JOB_WORKSPACE_ROOT|MIRROR_PATH|JOB_WORKSPACE|https://oauth2:' "${file}" || fail "stats workflow should not use retired paths or credential URLs"
}
test_shared_workspace_helper_is_removed() {
test ! -e "${REPO_ROOT}/.gitea/ci/bootstrap_workspace.sh" || fail "shared workspace helper should be removed"
}
test_register_default_capacity_is_four() {
grep -q "config\['runner'\]\['capacity'\] = 4" "${REPO_ROOT}/docker-runner/common/register.sh" || fail "register.sh should default new runner capacity to 4"
}
test_presets_do_not_mount_workspace_helper() {
! rg -q 'workspace\.sh:/data/workspace\.sh' "${REPO_ROOT}/docker-runner/presets" || fail "presets should not mount a shared workspace helper"
}
test_workflow_docs_and_links_use_actual_paths() {
local workflow_doc stats_workflow release_workflow
workflow_doc="${REPO_ROOT}/WORKFLOW.md"
stats_workflow="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
release_workflow="${REPO_ROOT}/.gitea/workflows/changelog_and_release.yml"
grep -q 'changelog_and_release.yml' "${workflow_doc}" || fail "WORKFLOW.md should mention changelog_and_release.yml"
grep -q 'update_stats_badge.yaml' "${workflow_doc}" || fail "WORKFLOW.md should mention update_stats_badge.yaml"
! rg -q 'changelog-and-release\.yml' "${workflow_doc}" || fail "WORKFLOW.md should not mention stale changelog-and-release.yml filename"
! rg -q 'update-stats-badge\.yml' "${workflow_doc}" || fail "WORKFLOW.md should not mention stale update-stats-badge.yml filename"
! rg -q 'update_stats_badge\.yml' "${workflow_doc}" || fail "WORKFLOW.md should not mention stale update_stats_badge.yml filename"
grep -q '/src/branch/${{ github.ref_name }}/.gitea/workflows/update_stats_badge.yaml' "${stats_workflow}" || fail "stats workflow summary should link to .gitea workflow path"
grep -q '/src/branch/${{ env.MAIN_BRANCH }}/.gitea/workflows/changelog_and_release.yml' "${release_workflow}" || fail "release workflow summary should link to .gitea workflow path"
! rg -q '/\\.github/workflows/' "${stats_workflow}" || fail "stats workflow should not link to .github/workflows"
! rg -q '/\\.github/workflows/' "${release_workflow}" || fail "release workflow should not link to .github/workflows"
}
test_workflow_doc_describes_workspace_architecture() {
local file
file="${REPO_ROOT}/WORKFLOW.md"
grep -Eq '^## .*运行模型' "${file}" || fail "WORKFLOW.md should include a run model section"
grep -Eq '固定.*slot|slot.*固定' "${file}" || fail "WORKFLOW.md should describe fixed workflow slots"
grep -Eq '职责|责任' "${file}" || fail "WORKFLOW.md should describe responsibility ownership"
grep -Eq '并发|串行' "${file}" || fail "WORKFLOW.md should describe slot serialization"
grep -Eq 'reset|重置' "${file}" || fail "WORKFLOW.md should describe reset before reuse"
grep -Eq '保留|复用' "${file}" || fail "WORKFLOW.md should describe persistent reuse"
! rg -q 'bare|MIRROR_ROOT|JOB_WORKSPACE_ROOT|/data/git-mirrors|/home/workspace/jobs' "${file}" || fail "WORKFLOW.md should not describe the retired mirror model"
! rg -q 'bootstrap_workspace\.sh' "${file}" || fail "WORKFLOW.md should describe architecture rather than helper implementation"
}
test_presets_do_not_mount_check_crlf_helper() {
! rg -q 'check_crlf\.sh:/data/check_crlf\.sh:ro' "${REPO_ROOT}/docker-runner/presets" || fail "preset compose files should not mount check_crlf helper into containers"
}
test_runner_data_is_gitignored() {
local path
for path in \
"docker-runner/presets/standard-ubuntu-22/runner-data/config.yaml" \
"docker-runner/presets/buildx-ubuntu-22/runner-data/config.yaml" \
"docker-runner/presets/buildx-archlinux/runner-data/config.yaml"; do
git -C "${REPO_ROOT}" check-ignore -q "${path}" || fail "${path} should be ignored as runtime runner data"
done
}
test_readme_has_project_intro_and_navigation() {
local file
file="${REPO_ROOT}/README.md"
grep -Eq '^## .*项目简介' "${file}" || fail "README.md should provide a project intro section"
grep -Eq '^## .*文档导航' "${file}" || fail "README.md should provide a document navigation section"
grep -q '\[DEPLOYMENT.md\](\./DEPLOYMENT.md)' "${file}" || fail "README.md should link to DEPLOYMENT.md"
grep -q '\[WORKFLOW.md\](\./WORKFLOW.md)' "${file}" || fail "README.md should link to WORKFLOW.md"
! rg -q '^## 🚀 快速提示$' "${file}" || fail "README.md should not carry concrete deployment tip sections"
! rg -q '^## ⚙️ 当前默认行为$' "${file}" || fail "README.md should not carry default behavior details"
! rg -q 'docker compose (build|up|exec)' "${file}" || fail "README.md should stay high-level and leave compose commands to deployment docs"
}
test_deployment_doc_stays_runner_focused() {
local file
file="${REPO_ROOT}/DEPLOYMENT.md"
! rg -q '^## ⚙️ 默认行为$' "${file}" || fail "DEPLOYMENT.md should not carry generic default behavior sections"
! rg -q '/data/git-mirrors/<owner>/<repo>\.git' "${file}" || fail "DEPLOYMENT.md should not document workflow mirror paths"
! rg -q '/home/workspace/jobs/' "${file}" || fail "DEPLOYMENT.md should not document workflow temp workspace paths"
! rg -q 'bootstrap_workspace\.sh' "${file}" || fail "DEPLOYMENT.md should stay focused on runner deployment, not workflow helpers"
}
test_presets_define_expected_hostname() {
grep -q '^ hostname: ubuntu$' "${REPO_ROOT}/docker-runner/presets/standard-ubuntu-22/docker-compose.yml" || fail "standard preset should set hostname to ubuntu"
grep -q '^ hostname: ubuntu$' "${REPO_ROOT}/docker-runner/presets/buildx-ubuntu-22/docker-compose.yml" || fail "buildx ubuntu preset should set hostname to ubuntu"
grep -q '^ hostname: arch$' "${REPO_ROOT}/docker-runner/presets/buildx-archlinux/docker-compose.yml" || fail "buildx arch preset should set hostname to arch"
}
test_preset_env_examples_exist() {
local file
for file in \
"${REPO_ROOT}/docker-runner/presets/standard-ubuntu-22/.env.example" \
"${REPO_ROOT}/docker-runner/presets/buildx-ubuntu-22/.env.example" \
"${REPO_ROOT}/docker-runner/presets/buildx-archlinux/.env.example"; do
test -f "${file}" || fail "missing env example: ${file}"
grep -q '^GITEA_INSTANCE=https://git.mytsl.cn$' "${file}" || fail "${file} should include company default instance"
grep -q '^GITEA_TOKEN=$' "${file}" || fail "${file} should include empty token placeholder"
grep -q '^DEFAULT_RUNNER_NAME=' "${file}" || fail "${file} should define DEFAULT_RUNNER_NAME override example"
grep -q '^DEFAULT_RUNNER_LABEL=' "${file}" || fail "${file} should define DEFAULT_RUNNER_LABEL override example"
done
}
test_preset_compose_supports_runner_identity_overrides() {
grep -q 'DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-standard-ubuntu-22}' \
"${REPO_ROOT}/docker-runner/presets/standard-ubuntu-22/docker-compose.yml" || \
fail "standard preset should allow DEFAULT_RUNNER_NAME override via env"
grep -q 'DEFAULT_RUNNER_LABEL=${DEFAULT_RUNNER_LABEL:-ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,standard-ubuntu-22:host://ubuntu:22.04}' \
"${REPO_ROOT}/docker-runner/presets/standard-ubuntu-22/docker-compose.yml" || \
fail "standard preset should allow DEFAULT_RUNNER_LABEL override via env"
grep -q 'DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-buildx-ubuntu-22}' \
"${REPO_ROOT}/docker-runner/presets/buildx-ubuntu-22/docker-compose.yml" || \
fail "buildx ubuntu preset should allow DEFAULT_RUNNER_NAME override via env"
grep -q 'DEFAULT_RUNNER_LABEL=${DEFAULT_RUNNER_LABEL:-ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,buildx-ubuntu-22:host://ubuntu:22.04}' \
"${REPO_ROOT}/docker-runner/presets/buildx-ubuntu-22/docker-compose.yml" || \
fail "buildx ubuntu preset should allow DEFAULT_RUNNER_LABEL override via env"
grep -q 'DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-buildx-archlinux}' \
"${REPO_ROOT}/docker-runner/presets/buildx-archlinux/docker-compose.yml" || \
fail "buildx arch preset should allow DEFAULT_RUNNER_NAME override via env"
grep -q 'DEFAULT_RUNNER_LABEL=${DEFAULT_RUNNER_LABEL:-archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest}' \
"${REPO_ROOT}/docker-runner/presets/buildx-archlinux/docker-compose.yml" || \
fail "buildx arch preset should allow DEFAULT_RUNNER_LABEL override via env"
}
test_register_requires_python_yaml_path() {
local file
file="${REPO_ROOT}/docker-runner/common/register.sh"
! rg -q 'Python configuration failed, using basic sed' "${file}" || fail "register.sh should not fall back to basic sed when Python config fails"
! rg -q 'Python3 not found, applying basic configuration' "${file}" || fail "register.sh should not continue with basic configuration when Python3 is missing"
! rg -q "sed -i 's/capacity: 1/capacity: 4/g' config.yaml" "${file}" || fail "register.sh should not mutate config.yaml via sed fallback"
grep -q "python3 << PYEOF" "${file}" || fail "register.sh should keep Python-based config generation"
}
test_setup_requires_upgrade_helper() {
local file
file="${REPO_ROOT}/docker-runner/common/setup.sh"
grep -q '^# shellcheck source=/dev/null$' "${file}" || fail "setup.sh should source upgrade.sh directly"
grep -q '^source "\${UPGRADE_HELPER}"$' "${file}" || fail "setup.sh should require sourcing upgrade.sh"
! grep -Fq 'if [ -f "${UPGRADE_HELPER}" ]; then' "${file}" || fail "setup.sh should not treat upgrade helper as optional"
! rg -q 'declare -F resolve_latest_version_or_fallback' "${file}" || fail "setup.sh should not guard helper functions with declare -F"
! rg -q 'declare -F validate_version' "${file}" || fail "setup.sh should not guard validate_version with declare -F"
! rg -q 'declare -F validate_binary_arch_or_fail' "${file}" || fail "setup.sh should not guard binary validation with declare -F"
}
test_entrypoint_uses_shared_buildx_builder_creation() {
local file
file="${REPO_ROOT}/docker-runner/common/entrypoint.sh"
grep -q '^create_buildx_builder() {$' "${file}" || fail "entrypoint.sh should extract Buildx builder creation into a helper"
grep -q '^ensure_buildx_builder() {$' "${file}" || fail "entrypoint.sh should extract Buildx builder selection into a helper"
grep -q '^[[:space:]]*create_buildx_builder$' "${file}" || fail "entrypoint.sh should use shared builder creation during initial setup"
grep -q '^[[:space:]]*ensure_buildx_builder$' "${file}" || fail "entrypoint.sh should use shared builder selection during reuse"
}
test_stats_workflow_avoids_eval_find() {
local file
file="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
! rg -q 'eval "find ' "${file}" || fail "stats workflow should avoid eval when building find commands"
grep -q 'EXCLUDE_FIND_ARGS=()' "${file}" || fail "stats workflow should build reusable find exclusion arrays"
grep -q 'LANG_FIND_ARGS=()' "${file}" || fail "stats workflow should build language-specific find arguments via arrays"
}
test_stats_workflow_uses_svg_badges() {
local file workflow_doc
file="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
workflow_doc="${REPO_ROOT}/WORKFLOW.md"
grep -q 'total-lines.svg' "${file}" || fail "stats workflow should generate total-lines.svg"
grep -q 'total-files.svg' "${file}" || fail "stats workflow should generate total-files.svg"
grep -q 'language-count.svg' "${file}" || fail "stats workflow should generate language-count.svg"
grep -Fq '{language}-lines.svg' "${file}" || fail "stats workflow should describe per-language svg badges"
grep -q '/src/branch/${{ env.BADGE_BRANCH }}/README.md' "${file}" || fail "stats workflow summary should link to stats branch root README"
! rg -q '/src/branch/\$\{\{ env.BADGE_BRANCH \}\}/\$\{\{ env.BADGE_DIR \}\}/README\.md' "${file}" || fail "stats workflow should not place README.md under the badge directory"
! rg -q 'img\.shields\.io/endpoint' "${file}" || fail "stats workflow should not depend on Shields endpoint badges"
! rg -q 'total-lines\.json|total-files\.json|\$\{lang_id\}-lines\.json|\$\{lang_id\}-files\.json' "${file}" || fail "stats workflow should not generate json badge payloads"
! rg -q 'GitHub Actions' "${file}" || fail "stats workflow should not mention GitHub Actions"
grep -q 'Gitea Actions' "${file}" || fail "stats workflow should mention Gitea Actions in generated content"
grep -q 'badges/cpp-lines.svg' "${workflow_doc}" || fail "WORKFLOW.md should show svg badge usage examples"
! rg -q 'img\.shields\.io/endpoint|cpp-lines\.json' "${workflow_doc}" || fail "WORKFLOW.md should not document json endpoint badge usage"
}
test_stats_workflow_uses_emoji_commit_messages() {
local file
file="${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"
grep -q 'git commit -m ":bar_chart: chore(stats): 初始化统计分支"' "${file}" || fail "stats workflow should initialize stats branch with an emoji commit message"
grep -q 'COMMIT_MSG=":bar_chart: chore(stats): 更新代码统计 \[' "${file}" || fail "stats workflow should use an emoji commit prefix for future stats updates"
}
test_entrypoint_uses_platform_aware_multiarch_verification() {
local file
file="${REPO_ROOT}/docker-runner/common/entrypoint.sh"
grep -q 'docker run --rm --platform linux/arm64 alpine uname -m' "${file}" || fail "entrypoint.sh should verify arm64 support with an explicit platform"
grep -q 'docker run --rm --platform linux/amd64 alpine uname -m' "${file}" || fail "entrypoint.sh should verify amd64 support with an explicit platform"
! rg -q 'docker run --rm arm64v8/alpine uname -m' "${file}" || fail "entrypoint.sh should not use arm64v8/alpine without an explicit platform"
! rg -q 'docker run --rm amd64/alpine uname -m' "${file}" || fail "entrypoint.sh should not use amd64/alpine without an explicit platform"
}
test_preset_compose_uses_env_for_instance
test_workflows_do_not_hardcode_company_server
test_stats_workflow_uses_workflow_secret_consistently
test_release_workflow_uses_inline_release_slot
test_stats_workflow_uses_inline_stats_slot
test_shared_workspace_helper_is_removed
test_register_default_capacity_is_four
test_presets_do_not_mount_workspace_helper
test_workflow_docs_and_links_use_actual_paths
test_workflow_doc_describes_workspace_architecture
test_presets_do_not_mount_check_crlf_helper
test_runner_data_is_gitignored
test_readme_has_project_intro_and_navigation
test_deployment_doc_stays_runner_focused
test_presets_define_expected_hostname
test_preset_env_examples_exist
test_preset_compose_supports_runner_identity_overrides
test_register_requires_python_yaml_path
test_setup_requires_upgrade_helper
test_entrypoint_uses_shared_buildx_builder_creation
test_stats_workflow_avoids_eval_find
test_stats_workflow_uses_svg_badges
test_stats_workflow_uses_emoji_commit_messages
test_entrypoint_uses_platform_aware_multiarch_verification
echo "template_defaults_test.sh: PASS"
-175
View File
@@ -1,175 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd)
# shellcheck source=/dev/null
source "${REPO_ROOT}/docker-runner/common/upgrade.sh"
assert_eq() {
local expected=$1
local actual=$2
local message=$3
if [ "${expected}" != "${actual}" ]; then
echo "FAIL: ${message}" >&2
echo " expected: ${expected}" >&2
echo " actual: ${actual}" >&2
exit 1
fi
}
make_fake_elf() {
local path=$1
local arch=$2
local machine_bytes=""
case "${arch}" in
amd64)
machine_bytes='\076\000'
;;
arm64)
machine_bytes='\267\000'
;;
arm-7)
machine_bytes='\050\000'
;;
*)
echo "FAIL: unsupported fake ELF arch ${arch}" >&2
exit 1
;;
esac
printf '\177ELF\002\001\001\000\000\000\000\000\000\000\000\000\002\000%b' \
"${machine_bytes}" > "${path}"
}
test_extract_versions_from_listing() {
local html
html=$(cat <<'EOF'
<tr class="file ">
<td class="name"><a href="/act_runner/0.6.1/"><span class="name">0.6.1</span></a></td>
</tr>
<tr class="file ">
<td class="name"><a href="/act_runner/0.5.0/"><span class="name">0.5.0</span></a></td>
</tr>
<tr class="file ">
<td class="name"><a href="/act_runner/0.4.1/"><span class="name">0.4.1</span></a></td>
</tr>
EOF
)
local actual
actual=$(extract_versions_from_listing <<<"${html}")
assert_eq $'0.6.1\n0.5.0\n0.4.1' "${actual}" \
"extract_versions_from_listing should return every version in listing order"
}
test_pick_latest_version() {
local actual
actual=$(pick_latest_version <<'EOF'
0.5.0
0.6.1
0.4.9
EOF
)
assert_eq "0.6.1" "${actual}" \
"pick_latest_version should return the highest semantic version"
}
test_extract_version_number_accepts_v_prefix() {
local actual
actual=$(extract_version_number <<'EOF'
act_runner version v0.6.1
EOF
)
assert_eq "0.6.1" "${actual}" \
"extract_version_number should accept version output with a leading v"
}
fake_latest_version() {
echo "0.6.2"
}
fake_invalid_version() {
echo "latest"
}
test_resolve_latest_version_or_fallback_prefers_latest() {
local actual
actual=$(resolve_latest_version_or_fallback "0.2.13" fake_latest_version)
assert_eq "0.6.2" "${actual}" \
"resolve_latest_version_or_fallback should prefer the detected latest version"
}
test_resolve_latest_version_or_fallback_uses_fallback() {
local actual
actual=$(resolve_latest_version_or_fallback "0.2.13" fake_invalid_version)
assert_eq "0.2.13" "${actual}" \
"resolve_latest_version_or_fallback should fall back when detected version is invalid"
}
test_detect_binary_arch_from_file() {
local temp_root amd64_file arm64_file arm7_file text_file
temp_root=$(mktemp -d)
amd64_file="${temp_root}/act_runner-amd64"
arm64_file="${temp_root}/act_runner-arm64"
arm7_file="${temp_root}/act_runner-arm7"
text_file="${temp_root}/not-elf"
make_fake_elf "${amd64_file}" "amd64"
make_fake_elf "${arm64_file}" "arm64"
make_fake_elf "${arm7_file}" "arm-7"
printf 'not an elf\n' > "${text_file}"
assert_eq "amd64" "$(detect_binary_arch_from_file "${amd64_file}")" \
"detect_binary_arch_from_file should recognize amd64 ELF binaries"
assert_eq "arm64" "$(detect_binary_arch_from_file "${arm64_file}")" \
"detect_binary_arch_from_file should recognize arm64 ELF binaries"
assert_eq "arm-7" "$(detect_binary_arch_from_file "${arm7_file}")" \
"detect_binary_arch_from_file should recognize arm-7 ELF binaries"
assert_eq "unknown" "$(detect_binary_arch_from_file "${text_file}")" \
"detect_binary_arch_from_file should reject non-ELF files"
rm -rf "${temp_root}"
}
test_binary_arch_matches_target() {
local temp_root amd64_file arm64_file
temp_root=$(mktemp -d)
amd64_file="${temp_root}/act_runner-amd64"
arm64_file="${temp_root}/act_runner-arm64"
make_fake_elf "${amd64_file}" "amd64"
make_fake_elf "${arm64_file}" "arm64"
if ! binary_arch_matches_target "${amd64_file}" "amd64"; then
echo "FAIL: binary_arch_matches_target should accept matching architectures" >&2
exit 1
fi
if binary_arch_matches_target "${arm64_file}" "amd64"; then
echo "FAIL: binary_arch_matches_target should reject mismatched architectures" >&2
exit 1
fi
rm -rf "${temp_root}"
}
test_extract_versions_from_listing
test_pick_latest_version
test_extract_version_number_accepts_v_prefix
test_resolve_latest_version_or_fallback_prefers_latest
test_resolve_latest_version_or_fallback_uses_fallback
test_detect_binary_arch_from_file
test_binary_arch_matches_target
echo "upgrade_test.sh: PASS"