♻️ 隔离 workflow 工作区并提升默认 runner 并发

This commit is contained in:
csh 2026-05-22 09:25:58 +08:00
parent 34480c16e2
commit ac05621817
7 changed files with 527 additions and 148 deletions

View File

@ -0,0 +1,215 @@
#!/bin/bash
workspace_fail() {
echo "bootstrap_workspace.sh: $*" >&2
return 1
}
split_repository_slug() {
local repo_slug=$1
local owner repo_name
if [[ "${repo_slug}" != */* ]]; then
workspace_fail "repository slug must look like owner/repo: ${repo_slug}"
return 1
fi
owner=${repo_slug%%/*}
repo_name=${repo_slug#*/}
printf '%s\n%s\n' "${owner}" "${repo_name}"
}
sanitize_job_name() {
local value=${1:-job}
value=$(printf '%s' "${value}" | tr ' /:@' '-----')
value=$(printf '%s' "${value}" | tr -cd '[:alnum:]._-')
value=$(printf '%s' "${value}" | sed -E 's/-+/-/g; s/^-//; s/-$//')
if [ -z "${value}" ]; then
value="job"
fi
printf '%s\n' "${value}"
}
build_job_identity() {
local run_id=${1:-0}
local run_attempt=${2:-1}
local job_name=${3:-job}
printf '%s-%s-%s\n' "${run_id}" "${run_attempt}" "${job_name}"
}
build_mirror_path() {
local mirror_root=$1
local owner=$2
local repo_name=$3
printf '%s/%s/%s.git\n' "${mirror_root}" "${owner}" "${repo_name}"
}
build_mirror_lock_path() {
local mirror_root=$1
local owner=$2
local repo_name=$3
printf '%s.lock\n' "$(build_mirror_path "${mirror_root}" "${owner}" "${repo_name}")"
}
build_job_workspace_root() {
local workspace_root=$1
local owner=$2
local repo_name=$3
local job_identity=$4
printf '%s/%s/%s/%s\n' "${workspace_root}" "${owner}" "${repo_name}" "${job_identity}"
}
build_job_repo_dir() {
local workspace_root=$1
local owner=$2
local repo_name=$3
local job_identity=$4
printf '%s/repo\n' "$(build_job_workspace_root "${workspace_root}" "${owner}" "${repo_name}" "${job_identity}")"
}
with_repo_lock() {
local lock_path=$1
shift
mkdir -p "$(dirname "${lock_path}")"
if ! command -v flock >/dev/null 2>&1; then
workspace_fail "flock is required for mirror synchronization"
return 1
fi
exec {lock_fd}>"${lock_path}"
flock "${lock_fd}"
"$@"
local status=$?
flock -u "${lock_fd}"
eval "exec ${lock_fd}>&-"
return "${status}"
}
ensure_bare_mirror_unlocked() {
local remote_url=$1
local mirror_path=$2
mkdir -p "$(dirname "${mirror_path}")"
if [ -d "${mirror_path}" ]; then
git -C "${mirror_path}" remote set-url origin "${remote_url}"
git -C "${mirror_path}" fetch --prune --prune-tags --tags origin
else
git clone --mirror "${remote_url}" "${mirror_path}" >/dev/null
fi
}
ensure_bare_mirror() {
local remote_url=$1
local mirror_path=$2
local lock_path
lock_path="${mirror_path}.lock"
with_repo_lock "${lock_path}" ensure_bare_mirror_unlocked "${remote_url}" "${mirror_path}"
}
create_job_workspace_from_mirror() {
local remote_url=$1
local mirror_path=$2
local job_workspace=$3
local repo_dir=$4
rm -rf "${job_workspace}"
mkdir -p "${job_workspace}"
git clone --local "${mirror_path}" "${repo_dir}" >/dev/null
git -C "${repo_dir}" remote set-url origin "${remote_url}"
}
cleanup_job_workspace() {
local job_workspace=$1
if [ -z "${job_workspace}" ] || [ "${job_workspace}" = "/" ]; then
workspace_fail "refusing to remove invalid workspace path: ${job_workspace}"
return 1
fi
rm -rf "${job_workspace}"
}
prepare_job_workspace() {
local repo_slug=$1
local remote_url=$2
local mirror_root=$3
local workspace_root=$4
local run_id=$5
local run_attempt=${6:-1}
local job_name=${7:-job}
local owner repo_name safe_job_name job_identity mirror_path job_workspace repo_dir
local slug_parts
mapfile -t slug_parts < <(split_repository_slug "${repo_slug}")
owner=${slug_parts[0]}
repo_name=${slug_parts[1]}
safe_job_name=$(sanitize_job_name "${job_name}")
job_identity=$(build_job_identity "${run_id}" "${run_attempt}" "${safe_job_name}")
mirror_path=$(build_mirror_path "${mirror_root}" "${owner}" "${repo_name}")
job_workspace=$(build_job_workspace_root "${workspace_root}" "${owner}" "${repo_name}" "${job_identity}")
repo_dir=$(build_job_repo_dir "${workspace_root}" "${owner}" "${repo_name}" "${job_identity}")
ensure_bare_mirror "${remote_url}" "${mirror_path}"
create_job_workspace_from_mirror "${remote_url}" "${mirror_path}" "${job_workspace}" "${repo_dir}"
printf 'REPO_OWNER=%s\n' "${owner}"
printf 'REPO_NAME=%s\n' "${repo_name}"
printf 'JOB_IDENTITY=%s\n' "${job_identity}"
printf 'MIRROR_PATH=%s\n' "${mirror_path}"
printf 'JOB_WORKSPACE=%s\n' "${job_workspace}"
printf 'REPO_DIR=%s\n' "${repo_dir}"
}
print_usage() {
cat <<'EOF'
Usage:
bootstrap_workspace.sh prepare-job-workspace <repo-slug> <remote-url> <mirror-root> <workspace-root> <run-id> <run-attempt> <job-name>
bootstrap_workspace.sh cleanup-job-workspace <job-workspace>
EOF
}
main() {
local command=${1:-}
case "${command}" in
prepare-job-workspace)
[ $# -eq 8 ] || {
print_usage
return 1
}
shift
prepare_job_workspace "$@"
;;
cleanup-job-workspace)
[ $# -eq 2 ] || {
print_usage
return 1
}
shift
cleanup_job_workspace "$1"
;;
*)
print_usage
return 1
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
set -euo pipefail
main "$@"
fi

View File

@ -31,8 +31,10 @@ env:
ACCESS_TOKEN: ${{ secrets.WORKFLOW }} ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
# ===== 工作区配置 ===== # ===== 工作区配置 =====
# 完整克隆的工作目录 - 自建的runner可以复用工作区 # 持久化 mirror 缓存目录
WORKSPACE_DIR: "/home/workspace" MIRROR_ROOT: "/data/git-mirrors"
# 每个 job 的独立临时工作目录根路径
JOB_WORKSPACE_ROOT: "/home/workspace/jobs"
# ===== 分支配置 ===== # ===== 分支配置 =====
# 主分支名称(用于推送 CHANGELOG 更新) # 主分支名称(用于推送 CHANGELOG 更新)
@ -228,87 +230,64 @@ jobs:
echo "======================================" echo "======================================"
echo "" echo ""
- name: 📥 克隆仓库 - name: 📥 准备隔离仓库
id: clone id: clone
run: | run: |
echo "======================================" echo "======================================"
echo "🚀 开始准备仓库" echo "🚀 开始准备仓库"
echo "======================================" echo "======================================"
REPO_SLUG="${{ github.repository }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
RUN_ATTEMPT="${{ github.run_attempt }}"
JOB_NAME="${{ github.job }}"
SERVER_HOST="${GITHUB_SERVER_URL#http://}"
SERVER_HOST="${SERVER_HOST#https://}"
REMOTE_URL="https://oauth2:${{ env.ACCESS_TOKEN }}@${SERVER_HOST}/${REPO_SLUG}.git"
BOOTSTRAP_SCRIPT="/tmp/bootstrap_workspace.sh"
BOOTSTRAP_URL="${GITHUB_SERVER_URL}/api/v1/repos/${REPO_SLUG}/media/.gitea/ci/bootstrap_workspace.sh?ref=${GITHUB_SHA}"
PREPARED_ENV=$(mktemp)
if [ -z "$RUN_ATTEMPT" ]; then
RUN_ATTEMPT="1"
fi
if [ -z "$JOB_NAME" ]; then
JOB_NAME="job"
fi
echo "📁 仓库名称: $REPO_NAME" echo "📁 仓库名称: $REPO_NAME"
echo "📍 目标目录: $REPO_DIR" echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}"
echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}"
echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo "" echo ""
# 检查仓库状态 curl -fsSL \
if [ -d "$REPO_DIR" ]; then -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \
echo "📂 目录已存在,检查 Git 仓库状态..." "$BOOTSTRAP_URL" \
-o "$BOOTSTRAP_SCRIPT"
chmod +x "$BOOTSTRAP_SCRIPT"
if [ -d "$REPO_DIR/.git" ]; then bash "$BOOTSTRAP_SCRIPT" prepare-job-workspace \
# 目录存在且是有效的 Git 仓库 "$REPO_SLUG" \
echo "✓ 发现有效的 Git 仓库,执行增量更新..." "$REMOTE_URL" \
cd "$REPO_DIR" "${{ env.MIRROR_ROOT }}" \
"${{ env.JOB_WORKSPACE_ROOT }}" \
"${{ github.run_id }}" \
"$RUN_ATTEMPT" \
"$JOB_NAME" > "$PREPARED_ENV"
# 清理工作区 # shellcheck source=/dev/null
git clean -fdx source "$PREPARED_ENV"
git reset --hard rm -f "$PREPARED_ENV"
# 获取最新代码和标签,同时清理远程已删除的 tag echo "✓ Mirror 路径: $MIRROR_PATH"
echo "📥 拉取最新代码和标签..." echo "✓ Job 工作区: $JOB_WORKSPACE"
git fetch --all --tags --force --prune --prune-tags echo "✓ 仓库目录: $REPO_DIR"
echo "✓ 已同步远程状态(包括已删除的 tag" echo ""
echo "✓ 仓库已更新"
else
# 目录存在但不是 Git 仓库(可能之前运行失败)
echo "⚠️ 目录存在但不是有效的 Git 仓库"
echo "🧹 清理损坏的目录..."
rm -rf "$REPO_DIR"
echo "✓ 已清理"
# 重新克隆
echo "📥 克隆仓库..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -ne 0 ]; then
echo "❌ 克隆失败"
cat /tmp/git_clone.log
# 清理残留
if [ -d "$REPO_DIR" ]; then
rm -rf "$REPO_DIR"
fi
exit 1
fi
cd "$REPO_DIR" cd "$REPO_DIR"
echo "✓ 仓库已克隆"
fi
else
# 目录不存在,首次克隆
echo "📥 克隆仓库(首次)..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -eq 0 ]; then
cd "$REPO_DIR"
echo "✓ 仓库已克隆"
else
echo "❌ 克隆失败"
exit 1
fi
fi
echo "" echo ""
echo "🔍 验证主分支配置..." echo "🔍 验证主分支配置..."
@ -325,7 +304,6 @@ jobs:
exit 1 exit 1
fi fi
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
echo "✓ 使用配置的主分支: $MAIN_BRANCH" echo "✓ 使用配置的主分支: $MAIN_BRANCH"
# 验证分支是否存在 # 验证分支是否存在
@ -351,7 +329,10 @@ jobs:
# 导出到环境变量供后续步骤使用 # 导出到环境变量供后续步骤使用
echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
echo "JOB_WORKSPACE=$JOB_WORKSPACE" >> $GITHUB_ENV
echo "MIRROR_PATH=$MIRROR_PATH" >> $GITHUB_ENV
echo "MAIN_BRANCH=$MAIN_BRANCH" >> $GITHUB_ENV echo "MAIN_BRANCH=$MAIN_BRANCH" >> $GITHUB_ENV
echo "BOOTSTRAP_SCRIPT=$BOOTSTRAP_SCRIPT" >> $GITHUB_ENV
echo "" echo ""
echo "✅ 仓库准备完成" echo "✅ 仓库准备完成"
@ -1078,16 +1059,6 @@ jobs:
检测到此提交由 Bot 创建(包含 `[skip ci]` 标记),为防止无限循环,已跳过执行。 检测到此提交由 Bot 创建(包含 `[skip ci]` 标记),为防止无限循环,已跳过执行。
EOFBOT EOFBOT
elif [ "${{ steps.check_version.outputs.version_exists }}" = "true" ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOFEXIST'
| 📋 执行状态 | ⏭️ 已跳过 (版本已存在) |
---
⏭️ **工作流已跳过**
版本 `${{ env.CHANGELOG_VERSION }}` 已存在于 CHANGELOG.md 中。
EOFEXIST
elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then
if [ "${{ steps.changelog.outputs.content_changed }}" = "true" ]; then if [ "${{ steps.changelog.outputs.content_changed }}" = "true" ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOFSUCCESS' cat >> $GITHUB_STEP_SUMMARY << 'EOFSUCCESS'
@ -1175,9 +1146,6 @@ jobs:
if [ "${{ steps.check_bot.outputs.is_bot_commit }}" = "true" ]; then if [ "${{ steps.check_bot.outputs.is_bot_commit }}" = "true" ]; then
echo "⏭️ 已跳过: Bot 提交检测" echo "⏭️ 已跳过: Bot 提交检测"
echo " 原因: 检测到 [skip ci] 标记,防止无限循环" echo " 原因: 检测到 [skip ci] 标记,防止无限循环"
elif [ "${{ steps.check_version.outputs.version_exists }}" = "true" ]; then
echo "⏭️ 已跳过: 版本已存在"
echo " 版本: ${{ env.CHANGELOG_VERSION }}"
elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then
echo "📊 执行结果:" echo "📊 执行结果:"
echo " - Tag: ${{ github.ref_name }}" echo " - Tag: ${{ github.ref_name }}"
@ -1223,4 +1191,8 @@ jobs:
run: | run: |
echo "🧹 清理临时文件..." echo "🧹 清理临时文件..."
rm -rf /tmp/commits.txt /tmp/changelog_updated.txt /tmp/content_changed.txt /tmp/release-body.txt /tmp/payload.json /tmp/release_response.json /tmp/upload_response_*.json rm -rf /tmp/commits.txt /tmp/changelog_updated.txt /tmp/content_changed.txt /tmp/release-body.txt /tmp/payload.json /tmp/release_response.json /tmp/upload_response_*.json
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}"
rm -rf "${{ env.JOB_WORKSPACE }}"
fi
echo "✅ 清理完成" echo "✅ 清理完成"

View File

@ -14,8 +14,10 @@ env:
ACCESS_TOKEN: ${{ secrets.WORKFLOW }} ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
# ===== 工作区配置 ===== # ===== 工作区配置 =====
# 完整克隆的工作目录 # 持久化 mirror 缓存目录
WORKSPACE_DIR: "/home/workspace" MIRROR_ROOT: "/data/git-mirrors"
# 每个 job 的独立临时工作目录根路径
JOB_WORKSPACE_ROOT: "/home/workspace/jobs"
# ===== 分支配置 ===== # ===== 分支配置 =====
# 徽章数据存储分支(可配置) # 徽章数据存储分支(可配置)
@ -177,80 +179,64 @@ jobs:
echo "======================================" echo "======================================"
echo "" echo ""
- name: 📥 克隆主仓库 - name: 📥 准备隔离仓库
id: clone_main id: clone_main
run: | run: |
echo "======================================" echo "======================================"
echo "🚀 开始准备主仓库" echo "🚀 开始准备主仓库"
echo "======================================" echo "======================================"
REPO_SLUG="${{ github.repository }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" RUN_ATTEMPT="${{ github.run_attempt }}"
JOB_NAME="${{ github.job }}"
SERVER_HOST="${GITHUB_SERVER_URL#http://}"
SERVER_HOST="${SERVER_HOST#https://}"
REMOTE_URL="https://oauth2:${{ env.ACCESS_TOKEN }}@${SERVER_HOST}/${REPO_SLUG}.git"
BOOTSTRAP_SCRIPT="/tmp/bootstrap_workspace.sh"
BOOTSTRAP_URL="${GITHUB_SERVER_URL}/api/v1/repos/${REPO_SLUG}/media/.gitea/ci/bootstrap_workspace.sh?ref=${GITHUB_SHA}"
PREPARED_ENV=$(mktemp)
if [ -z "$RUN_ATTEMPT" ]; then
RUN_ATTEMPT="1"
fi
if [ -z "$JOB_NAME" ]; then
JOB_NAME="job"
fi
echo "📁 仓库名称: $REPO_NAME" echo "📁 仓库名称: $REPO_NAME"
echo "📍 目标目录: $REPO_DIR" echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}"
echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}"
echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo "🌿 分支: ${{ github.ref_name }}" echo "🌿 分支: ${{ github.ref_name }}"
echo "" echo ""
# 检查仓库状态 curl -fsSL \
if [ -d "$REPO_DIR" ]; then -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \
echo "📂 目录已存在,检查 Git 仓库状态..." "$BOOTSTRAP_URL" \
-o "$BOOTSTRAP_SCRIPT"
chmod +x "$BOOTSTRAP_SCRIPT"
if [ -d "$REPO_DIR/.git" ]; then bash "$BOOTSTRAP_SCRIPT" prepare-job-workspace \
# 目录存在且是有效的 Git 仓库 "$REPO_SLUG" \
echo "✓ 发现有效的 Git 仓库,执行增量更新..." "$REMOTE_URL" \
cd "$REPO_DIR" "${{ env.MIRROR_ROOT }}" \
"${{ env.JOB_WORKSPACE_ROOT }}" \
"${{ github.run_id }}" \
"$RUN_ATTEMPT" \
"$JOB_NAME" > "$PREPARED_ENV"
# 清理工作区 # shellcheck source=/dev/null
git clean -fdx source "$PREPARED_ENV"
git reset --hard rm -f "$PREPARED_ENV"
# 获取最新代码 echo "✓ Mirror 路径: $MIRROR_PATH"
echo "📥 拉取最新代码..." echo "✓ Job 工作区: $JOB_WORKSPACE"
git fetch --all --tags --force echo "✓ 仓库目录: $REPO_DIR"
echo ""
echo "✓ 仓库已更新"
else
# 目录存在但不是 Git 仓库(可能之前运行失败)
echo "⚠️ 目录存在但不是有效的 Git 仓库"
echo "🧹 清理损坏的目录..."
rm -rf "$REPO_DIR"
echo "✓ 已清理"
# 重新克隆
echo "📥 克隆仓库..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -ne 0 ]; then
echo "❌ 克隆失败"
exit 1
fi
cd "$REPO_DIR" cd "$REPO_DIR"
echo "✓ 仓库已克隆"
fi
else
# 目录不存在,首次克隆
echo "📥 克隆仓库(首次)..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -ne 0 ]; then
echo "❌ 克隆失败"
exit 1
fi
cd "$REPO_DIR"
echo "✓ 仓库已克隆"
fi
# 切换到目标分支 # 切换到目标分支
echo "🏷️ 切换到分支: ${{ github.ref_name }}" echo "🏷️ 切换到分支: ${{ github.ref_name }}"
@ -274,6 +260,9 @@ jobs:
# 导出环境变量 # 导出环境变量
echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
echo "JOB_WORKSPACE=$JOB_WORKSPACE" >> $GITHUB_ENV
echo "MIRROR_PATH=$MIRROR_PATH" >> $GITHUB_ENV
echo "BOOTSTRAP_SCRIPT=$BOOTSTRAP_SCRIPT" >> $GITHUB_ENV
echo "✅ 主仓库准备完成" echo "✅ 主仓库准备完成"
echo "======================================" echo "======================================"
@ -964,7 +953,9 @@ jobs:
if [ "${{ env.CLEANUP_WORKSPACE }}" == "true" ]; then if [ "${{ env.CLEANUP_WORKSPACE }}" == "true" ]; then
echo "" echo ""
echo "🧹 清理工作区..." echo "🧹 清理工作区..."
rm -rf "${{ env.WORKSPACE_DIR }}" if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
rm -rf "${{ env.JOB_WORKSPACE }}"
fi
echo "✅ 清理完成" echo "✅ 清理完成"
fi fi
@ -973,4 +964,8 @@ jobs:
run: | run: |
echo "🧹 清理临时文件..." echo "🧹 清理临时文件..."
rm -rf /tmp/lang_stats /tmp/lang_summary.txt /tmp/total_stats*.json rm -rf /tmp/lang_stats /tmp/lang_summary.txt /tmp/total_stats*.json
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}"
rm -rf "${{ env.JOB_WORKSPACE }}"
fi
echo "✅ 清理完成" echo "✅ 清理完成"

View File

@ -33,6 +33,9 @@ docker-runner/
│ ├── register.sh # Runner 注册脚本 │ ├── register.sh # Runner 注册脚本
│ └── manage.sh # Runner 管理脚本 │ └── manage.sh # Runner 管理脚本
├── .gitea/ci/
│ └── bootstrap_workspace.sh # workflow 自举脚本(下载后准备 mirror 和独立工作区)
└── presets/ # 预设配置(选择一个使用) └── presets/ # 预设配置(选择一个使用)
├── standard-ubuntu-22/ # 标准版 (Ubuntu 22.04) ├── standard-ubuntu-22/ # 标准版 (Ubuntu 22.04)
│ ├── Dockerfile │ ├── Dockerfile
@ -49,7 +52,14 @@ docker-runner/
- `common/` 目录中的脚本由所有版本共享,通过 docker-compose.yml 挂载到容器 - `common/` 目录中的脚本由所有版本共享,通过 docker-compose.yml 挂载到容器
- `presets/` 目录中每个子目录是一个完整的预设配置,包含 Dockerfile 和 docker-compose.yml - `presets/` 目录中每个子目录是一个完整的预设配置,包含 Dockerfile 和 docker-compose.yml
- 数据持久化在 `runner-data/` 目录(自动创建),包含 runner 配置、缓存和 act_runner 二进制文件 - 数据持久化在 `runner-data/` 目录(自动创建),包含 runner 配置、mirror 缓存和 act_runner 二进制文件
### 并发与工作区模型
- 新注册的 runner 默认 `capacity=4`
- 共享仓库缓存保存在 `/data/git-mirrors/<owner>/<repo>.git`
- 每个 job 会从 mirror 本地克隆到独立目录 `/home/workspace/jobs/<owner>/<repo>/<job-identity>/repo`
- job 结束后临时工作目录会自动清理mirror 会保留以加速后续大仓库同步
--- ---
@ -139,7 +149,7 @@ docker compose exec gitea-runner /data/register.sh
输入你的 Gitea 实例 URL 和注册令牌。 输入你的 Gitea 实例 URL 和注册令牌。
Runner 注册后会自动启动,无需重启容器。 Runner 注册后会自动启动,无需重启容器。新注册的 runner 默认并发为 `4`
#### 6. 验证运行状态 #### 6. 验证运行状态
@ -577,6 +587,9 @@ docker system df
runner-data/ runner-data/
├── bin/ ├── bin/
│ └── act_runner # Runner 可执行文件(持久化) │ └── act_runner # Runner 可执行文件(持久化)
├── git-mirrors/ # 持久化 bare mirror 缓存
│ └── <owner>/
│ └── <repo>.git
├── runners/ # Runner 配置目录 ├── runners/ # Runner 配置目录
│ └── <runner-name>/ │ └── <runner-name>/
│ ├── .runner # 注册信息 │ ├── .runner # 注册信息
@ -586,7 +599,7 @@ runner-data/
└── .configured └── .configured
``` ```
容器重启或重建后,数据不会丢失。 容器重启或重建后,数据不会丢失。workflow 的临时工作目录位于 `/home/workspace/jobs/...`,任务结束后会自动清理,不会作为持久化数据保留。
### Q10: 如何切换不同预设? ### Q10: 如何切换不同预设?

View File

@ -13,6 +13,13 @@
- 📚 **文档规范**:统一的文档格式和版本管理规范 - 📚 **文档规范**:统一的文档格式和版本管理规范
- 🔧 **配置指南**:详细的配置说明和最佳实践 - 🔧 **配置指南**:详细的配置说明和最佳实践
## ⚙️ 当前默认行为
- 新注册的 runner 默认 `capacity=4`
- 大仓库会缓存到 `/data/git-mirrors/<owner>/<repo>.git`
- 每个 workflow job 使用独立临时目录 `/home/workspace/jobs/<owner>/<repo>/<job-identity>/repo`
- job 结束后会自动清理临时工作目录mirror 缓存保留在 `runner-data/`
## 📂 文档导航 ## 📂 文档导航
### 🚀 Runner ### 🚀 Runner

View File

@ -161,7 +161,7 @@ try:
# 使用实际注册的 labels # 使用实际注册的 labels
config['runner']['labels'] = registered_labels config['runner']['labels'] = registered_labels
config['runner']['capacity'] = 2 config['runner']['capacity'] = 4
# 启用缓存 # 启用缓存
if 'cache' not in config: if 'cache' not in config:
@ -175,7 +175,7 @@ try:
print("✓ Configuration updated using Python") print("✓ Configuration updated using Python")
print(f" - Labels: {registered_labels}") print(f" - Labels: {registered_labels}")
print(f" - Capacity: 2") print(f" - Capacity: 4")
print(f" - Cache enabled: ./cache") print(f" - Cache enabled: ./cache")
sys.exit(0) sys.exit(0)
@ -191,7 +191,7 @@ PYEOF
echo "⚠ Python configuration failed, using basic sed..." echo "⚠ Python configuration failed, using basic sed..."
# 基本的 sed 修改(只修改简单的值,不动 labels # 基本的 sed 修改(只修改简单的值,不动 labels
sed -i 's/capacity: 1/capacity: 2/g' config.yaml || true sed -i 's/capacity: 1/capacity: 4/g' config.yaml || true
sed -i 's/enabled: false/enabled: true/g' config.yaml || true sed -i 's/enabled: false/enabled: true/g' config.yaml || true
sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true
@ -202,7 +202,7 @@ else
echo "⚠ Python3 not found, applying basic configuration..." echo "⚠ Python3 not found, applying basic configuration..."
# 基本的 sed 修改 # 基本的 sed 修改
sed -i 's/capacity: 1/capacity: 2/g' config.yaml || true sed -i 's/capacity: 1/capacity: 4/g' config.yaml || true
sed -i 's/enabled: false/enabled: true/g' config.yaml || true sed -i 's/enabled: false/enabled: true/g' config.yaml || true
sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true

View File

@ -0,0 +1,177 @@
#!/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}/.gitea/ci/bootstrap_workspace.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
}
test_repo_path_layout() {
local owner repo_name mirror_dir workspace_root mirror_path workspace_root_path repo_dir
owner="csh"
repo_name="actions-template"
mirror_dir="/data/git-mirrors"
workspace_root="/home/workspace/jobs"
mirror_path=$(build_mirror_path "${mirror_dir}" "${owner}" "${repo_name}")
workspace_root_path=$(build_job_workspace_root "${workspace_root}" "${owner}" "${repo_name}" "123456-1-release")
repo_dir=$(build_job_repo_dir "${workspace_root}" "${owner}" "${repo_name}" "123456-1-release")
assert_eq "/data/git-mirrors/csh/actions-template.git" "${mirror_path}" "mirror path should include owner and bare repo suffix"
assert_eq "/home/workspace/jobs/csh/actions-template/123456-1-release" "${workspace_root_path}" "workspace root should include owner repo and job identity"
assert_eq "/home/workspace/jobs/csh/actions-template/123456-1-release/repo" "${repo_dir}" "repo dir should live under isolated workspace root"
}
test_job_identity_prefers_run_metadata() {
local actual
actual=$(build_job_identity "123456" "2" "release-job")
assert_eq "123456-2-release-job" "${actual}" "job identity should include run id attempt and job name"
}
test_sanitize_job_name() {
local actual
actual=$(sanitize_job_name "release notes/job")
assert_eq "release-notes-job" "${actual}" "job names should be filesystem-safe"
}
test_build_lock_path() {
local actual
actual=$(build_mirror_lock_path "/data/git-mirrors" "csh" "actions-template")
assert_eq "/data/git-mirrors/csh/actions-template.git.lock" "${actual}" "lock path should sit beside the bare mirror"
}
test_repo_owner_and_name_parsing() {
local actual
actual=$(split_repository_slug "csh/actions-template")
assert_eq $'csh\nactions-template' "${actual}" "repository slug should split into owner and repo lines"
}
test_prepare_and_cleanup_workspace() {
local temp_root remote_repo seed_repo mirror_root workspace_root env_file
local repo_dir mirror_path job_workspace origin_url
temp_root=$(mktemp -d)
remote_repo="${temp_root}/remote.git"
seed_repo="${temp_root}/seed"
mirror_root="${temp_root}/mirrors"
workspace_root="${temp_root}/jobs"
env_file="${temp_root}/prepared.env"
git init -b main "${seed_repo}" >/dev/null
git -C "${seed_repo}" config user.name "Test User"
git -C "${seed_repo}" config user.email "test@example.com"
printf 'hello\n' > "${seed_repo}/README.md"
git -C "${seed_repo}" add README.md
git -C "${seed_repo}" commit -m "Initial commit" >/dev/null
git clone --bare "${seed_repo}" "${remote_repo}" >/dev/null
prepare_job_workspace \
"csh/actions-template" \
"${remote_repo}" \
"${mirror_root}" \
"${workspace_root}" \
"123456" \
"2" \
"release job" > "${env_file}"
# shellcheck source=/dev/null
source "${env_file}"
repo_dir="${REPO_DIR}"
mirror_path="${MIRROR_PATH}"
job_workspace="${JOB_WORKSPACE}"
if [ ! -d "${mirror_path}" ]; then
echo "FAIL: mirror path should exist after preparation" >&2
exit 1
fi
if [ ! -d "${repo_dir}/.git" ]; then
echo "FAIL: prepared repo dir should contain a git checkout" >&2
exit 1
fi
origin_url=$(git -C "${repo_dir}" remote get-url origin)
assert_eq "${remote_repo}" "${origin_url}" "prepared repo should point origin to the requested remote"
cleanup_job_workspace "${job_workspace}"
if [ -e "${job_workspace}" ]; then
echo "FAIL: cleanup should remove the job workspace" >&2
exit 1
fi
rm -rf "${temp_root}"
}
test_register_default_capacity_is_four() {
if ! grep -q "config\['runner'\]\['capacity'\] = 4" "${REPO_ROOT}/docker-runner/common/register.sh"; then
echo "FAIL: register.sh should default new runner capacity to 4" >&2
exit 1
fi
}
test_changelog_workflow_uses_workspace_helper() {
if ! grep -q ".gitea/ci/bootstrap_workspace.sh" "${REPO_ROOT}/.gitea/workflows/changelog_and_release.yml"; then
echo "FAIL: changelog workflow should fetch repo-owned bootstrap helper" >&2
exit 1
fi
}
test_stats_workflow_uses_workspace_helper() {
if ! grep -q ".gitea/ci/bootstrap_workspace.sh" "${REPO_ROOT}/.gitea/workflows/update_stats_badge.yaml"; then
echo "FAIL: stats workflow should fetch repo-owned bootstrap helper" >&2
exit 1
fi
}
test_presets_do_not_mount_workspace_helper() {
if rg -q "workspace\.sh:/data/workspace\.sh" "${REPO_ROOT}/docker-runner/presets"; then
echo "FAIL: preset compose files should not mount workspace helper from runner common" >&2
exit 1
fi
}
test_docs_mention_git_mirrors() {
if ! grep -q "/data/git-mirrors" "${REPO_ROOT}/DEPLOYMENT.md"; then
echo "FAIL: deployment docs should describe persistent git mirrors" >&2
exit 1
fi
}
test_repo_path_layout
test_job_identity_prefers_run_metadata
test_sanitize_job_name
test_build_lock_path
test_repo_owner_and_name_parsing
test_prepare_and_cleanup_workspace
test_register_default_capacity_is_four
test_changelog_workflow_uses_workspace_helper
test_stats_workflow_uses_workspace_helper
test_presets_do_not_mount_workspace_helper
test_docs_mention_git_mirrors
echo "workspace_helper_test.sh: PASS"