♻️ refactor(ci): use fixed workflow slots

This commit is contained in:
csh
2026-07-16 09:17:05 +08:00
parent 778297543a
commit 610c2b3e74
7 changed files with 210 additions and 570 deletions
-207
View File
@@ -1,207 +0,0 @@
#!/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_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 --shared "${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
+57 -98
View File
@@ -29,16 +29,21 @@ env:
# - GIT_TOKEN: 用于 git clone/push # - GIT_TOKEN: 用于 git clone/push
# - API_TOKEN: 用于 API 调用 # - API_TOKEN: 用于 API 调用
ACCESS_TOKEN: ${{ secrets.WORKFLOW }} ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
GIT_CONFIG_COUNT: "3"
GIT_CONFIG_KEY_0: "credential.helper"
GIT_CONFIG_VALUE_0: ""
GIT_CONFIG_KEY_1: "credential.helper"
GIT_CONFIG_VALUE_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"
# ===== 工作区配置 ===== # ===== 工作区配置 =====
# 持久化 mirror 缓存目录 WORKSPACE_DIR: "/data/workspace"
MIRROR_ROOT: "/data/git-mirrors" WORKSPACE_SLOT: "release"
# 每个 job 的独立临时工作目录根路径
JOB_WORKSPACE_ROOT: "/home/workspace/jobs"
# ===== 分支配置 ===== # ===== 分支配置 =====
# 主分支名称(用于推送 CHANGELOG 更新) # 主分支名称(用于推送 CHANGELOG 更新)
# 如果留空,会自动检测(main 或 master # 必须明确配置主分支(例如 main 或 master
MAIN_BRANCH: "main" MAIN_BRANCH: "main"
# ===== 服务器配置 ===== # ===== 服务器配置 =====
@@ -233,6 +238,7 @@ jobs:
- name: 📥 准备隔离仓库 - name: 📥 准备隔离仓库
id: clone id: clone
run: | run: |
set -euo pipefail
echo "======================================" echo "======================================"
echo "🚀 开始准备仓库" echo "🚀 开始准备仓库"
echo "======================================" echo "======================================"
@@ -240,106 +246,69 @@ jobs:
REPO_SLUG="${{ github.repository }}" REPO_SLUG="${{ github.repository }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
MAIN_BRANCH="${{ env.MAIN_BRANCH }}" MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
RUN_ATTEMPT="${{ github.run_attempt }}" WORKSPACE_DIR="${{ env.WORKSPACE_DIR }}"
JOB_NAME="${{ github.job }}" WORKSPACE_SLOT="${{ env.WORKSPACE_SLOT }}"
SERVER_HOST="${GITHUB_SERVER_URL#http://}" REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"
SERVER_HOST="${SERVER_HOST#https://}" REMOTE_URL="${GITHUB_SERVER_URL}/${REPO_SLUG}.git"
REMOTE_URL="https://oauth2:${{ env.ACCESS_TOKEN }}@${SERVER_HOST}/${REPO_SLUG}.git" REMOTE_REF="refs/remotes/origin/${MAIN_BRANCH}"
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 if [ -z "$MAIN_BRANCH" ]; then
RUN_ATTEMPT="1" echo "❌ 错误: 未配置主分支"
exit 1
fi fi
case "$REMOTE_URL" in
if [ -z "$JOB_NAME" ]; then https://*) ;;
JOB_NAME="job" *)
echo "❌ 拒绝通过非 HTTPS remote 使用 WORKFLOW token" >&2
exit 1
;;
esac
if [ -e "$REPO_DIR" ] && [ ! -d "$REPO_DIR/.git" ]; then
echo "❌ Slot 已存在但不是 Git 仓库: $REPO_DIR" >&2
exit 1
fi fi
echo "📁 仓库名称: $REPO_NAME" echo "📁 仓库名称: $REPO_NAME"
echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" echo "📦 工作区根目录: $WORKSPACE_DIR"
echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" echo "🎯 Workflow 职责: $WORKSPACE_SLOT"
echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo "" echo ""
curl -fsSL \ if [ ! -d "$REPO_DIR/.git" ]; then
-H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ mkdir -p "$(dirname "$REPO_DIR")"
"$BOOTSTRAP_URL" \ git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"
-o "$BOOTSTRAP_SCRIPT" else
chmod +x "$BOOTSTRAP_SCRIPT" git -C "$REPO_DIR" remote set-url origin "$REMOTE_URL"
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
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
bash "$BOOTSTRAP_SCRIPT" prepare-job-workspace \
"$REPO_SLUG" \
"$REMOTE_URL" \
"${{ env.MIRROR_ROOT }}" \
"${{ env.JOB_WORKSPACE_ROOT }}" \
"${{ github.run_id }}" \
"$RUN_ATTEMPT" \
"$JOB_NAME" > "$PREPARED_ENV"
# shellcheck source=/dev/null
source "$PREPARED_ENV"
rm -f "$PREPARED_ENV"
echo "✓ Mirror 路径: $MIRROR_PATH"
echo "✓ Job 工作区: $JOB_WORKSPACE"
echo "✓ 仓库目录: $REPO_DIR" echo "✓ 仓库目录: $REPO_DIR"
echo "" echo ""
cd "$REPO_DIR" cd "$REPO_DIR"
echo "✓ 主分支 '$MAIN_BRANCH' 已准备并清理"
echo ""
echo "🔍 验证主分支配置..."
# 检查是否配置了主分支
if [ -z "${{ env.MAIN_BRANCH }}" ]; then
echo "❌ 错误: 未配置主分支"
echo ""
echo "请在 workflow 配置文件的 env 区域设置 MAIN_BRANCH:"
echo ""
echo "env:"
echo " MAIN_BRANCH: \"main\" # 或 \"master\""
echo ""
exit 1
fi
echo "✓ 使用配置的主分支: $MAIN_BRANCH"
# 验证分支是否存在
if ! git show-ref --verify --quiet refs/remotes/origin/$MAIN_BRANCH; then
echo "❌ 错误: 配置的分支 '$MAIN_BRANCH' 不存在"
echo ""
echo "可用的远程分支:"
git branch -r | grep -v HEAD
echo ""
echo "请在 workflow 配置文件中修改 MAIN_BRANCH 为正确的分支名"
exit 1
fi
echo "✓ 分支 '$MAIN_BRANCH' 已验证存在"
# 切换到主分支
echo ""
echo "🌿 切换到主分支: $MAIN_BRANCH"
git checkout $MAIN_BRANCH
git pull origin $MAIN_BRANCH
echo "✓ 已切换到主分支并更新到最新"
# 导出到环境变量供后续步骤使用 # 导出到环境变量供后续步骤使用
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 "WORKSPACE_SLOT=$WORKSPACE_SLOT" >> "$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 "✅ 仓库准备完成"
echo "======================================" echo "======================================"
echo "" echo ""
- name: 🔍 检查 Bot 提交 - name: 🤖 检查 Bot 提交
id: check_bot id: check_bot
run: | run: |
echo "======================================" echo "======================================"
@@ -829,9 +798,7 @@ jobs:
git commit -m "${{ env.COMMIT_MSG }}" git commit -m "${{ env.COMMIT_MSG }}"
echo "📤 推送到远程 ${{ env.MAIN_BRANCH }} 分支..." echo "📤 推送到远程 ${{ env.MAIN_BRANCH }} 分支..."
git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.MAIN_BRANCH }} if git -C "${{ env.REPO_DIR }}" push origin "${{ env.MAIN_BRANCH }}"; then
if [ $? -eq 0 ]; then
echo "✅ 推送成功" echo "✅ 推送成功"
else else
echo "❌ 推送失败" echo "❌ 推送失败"
@@ -853,7 +820,7 @@ jobs:
cd ${{ env.REPO_DIR }} cd ${{ env.REPO_DIR }}
# 确保使用最新的 CHANGELOG # 确保使用最新的 CHANGELOG
git pull origin ${{ env.MAIN_BRANCH }} git -C "${{ env.REPO_DIR }}" pull --ff-only origin "${{ env.MAIN_BRANCH }}"
python3 << 'PYSCRIPT' python3 << 'PYSCRIPT'
import re import re
@@ -1029,11 +996,11 @@ jobs:
echo "======================================" echo "======================================"
echo "" echo ""
- name: 📊 生成 Workflow Summary - name: 📋 生成 Workflow Summary
if: always() if: always()
run: | run: |
echo "======================================" echo "======================================"
echo "📊 生成 Workflow Summary" echo "📋 生成 Workflow Summary"
echo "======================================" echo "======================================"
cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY' cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY'
@@ -1071,7 +1038,6 @@ jobs:
- ✅ CHANGELOG.md 已更新 - ✅ CHANGELOG.md 已更新
- ✅ 更改已推送到主分支 (`${{ env.MAIN_BRANCH }}`) - ✅ 更改已推送到主分支 (`${{ env.MAIN_BRANCH }}`)
- ✅ Release 已创建 - ✅ Release 已创建
- ✅ CHANGELOG.md 已作为附件上传
EOFSUCCESS EOFSUCCESS
else else
cat >> $GITHUB_STEP_SUMMARY << 'EOFREBUILD' cat >> $GITHUB_STEP_SUMMARY << 'EOFREBUILD'
@@ -1083,7 +1049,6 @@ jobs:
- ️ CHANGELOG.md 无需更新(内容已存在) - ️ CHANGELOG.md 无需更新(内容已存在)
- ✅ Release 已创建 - ✅ Release 已创建
- ✅ CHANGELOG.md 已作为附件上传
💡 **说明**: 检测到这是重建已删除的 tag,CHANGELOG 中已包含所有提交内容,因此直接从现有内容创建 Release。 💡 **说明**: 检测到这是重建已删除的 tag,CHANGELOG 中已包含所有提交内容,因此直接从现有内容创建 Release。
EOFREBUILD EOFREBUILD
@@ -1159,11 +1124,9 @@ jobs:
echo "✅ 已完成的操作:" echo "✅ 已完成的操作:"
echo " - CHANGELOG.md 已更新并推送到 ${{ env.MAIN_BRANCH }} 分支" echo " - CHANGELOG.md 已更新并推送到 ${{ env.MAIN_BRANCH }} 分支"
echo " - Release 已创建" echo " - Release 已创建"
echo " - CHANGELOG.md 已上传为附件"
else else
echo "✅ 已完成的操作:" echo "✅ 已完成的操作:"
echo " - Release 已创建(从现有 CHANGELOG 内容)" echo " - Release 已创建(从现有 CHANGELOG 内容)"
echo " - CHANGELOG.md 已上传为附件"
echo "" echo ""
echo "💡 说明: 检测到这是重建已删除的 tag,CHANGELOG 中已包含" echo "💡 说明: 检测到这是重建已删除的 tag,CHANGELOG 中已包含"
echo " 所有提交内容,因此直接从现有内容创建 Release。" echo " 所有提交内容,因此直接从现有内容创建 Release。"
@@ -1191,8 +1154,4 @@ 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 "✅ 清理完成"
+9 -9
View File
@@ -11,14 +11,14 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: 打印问候 - name: 👋 打印问候
run: | run: |
echo "=====================================" echo "====================================="
echo " Ubuntu System Information Check " echo " Ubuntu System Information Check "
echo "=====================================" echo "====================================="
echo "" echo ""
- name: 显示系统信息 - name: 🖥️ 显示系统信息
run: | run: |
echo "===== 系统版本 =====" echo "===== 系统版本 ====="
cat /etc/os-release cat /etc/os-release
@@ -32,7 +32,7 @@ jobs:
echo "===== 系统启动时间 =====" echo "===== 系统启动时间 ====="
uptime uptime
- name: 显示 CPU 信息 - name: 🧮 显示 CPU 信息
run: | run: |
echo "===== CPU 详细信息 =====" echo "===== CPU 详细信息 ====="
lscpu lscpu
@@ -43,7 +43,7 @@ jobs:
echo "===== CPU 核心数 =====" echo "===== CPU 核心数 ====="
nproc nproc
- name: 显示内存信息 - name: 🧠 显示内存信息
run: | run: |
echo "===== 内存使用情况 =====" echo "===== 内存使用情况 ====="
free -h free -h
@@ -51,7 +51,7 @@ jobs:
echo "===== 内存详细信息 =====" echo "===== 内存详细信息 ====="
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable' cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable'
- name: 显示磁盘信息 - name: 💾 显示磁盘信息
run: | run: |
echo "===== 磁盘使用情况 =====" echo "===== 磁盘使用情况 ====="
df -h df -h
@@ -59,7 +59,7 @@ jobs:
echo "===== 磁盘分区信息 =====" echo "===== 磁盘分区信息 ====="
lsblk lsblk
- name: 显示网络信息 - name: 🌐 显示网络信息
run: | run: |
echo "===== 网络接口 =====" echo "===== 网络接口 ====="
cat /proc/net/dev cat /proc/net/dev
@@ -70,7 +70,7 @@ jobs:
echo "===== DNS 配置 =====" echo "===== DNS 配置 ====="
cat /etc/resolv.conf cat /etc/resolv.conf
- name: 显示已安装的软件 - name: 📦 显示已安装的软件
run: | run: |
echo "===== Python 版本 =====" echo "===== Python 版本 ====="
python3 --version python3 --version
@@ -84,7 +84,7 @@ jobs:
echo "===== Git 版本 =====" echo "===== Git 版本 ====="
git --version git --version
- name: 显示环境变量 - name: 🔧 显示环境变量
run: | run: |
echo "===== 重要环境变量 =====" echo "===== 重要环境变量 ====="
echo "PATH: $PATH" echo "PATH: $PATH"
@@ -93,7 +93,7 @@ jobs:
echo "SHELL: $SHELL" echo "SHELL: $SHELL"
echo "PWD: $PWD" echo "PWD: $PWD"
- name: 显示当前目录 - name: 📂 显示当前目录
run: | run: |
echo "===== 当前工作目录 =====" echo "===== 当前工作目录 ====="
pwd pwd
+68 -76
View File
@@ -5,6 +5,10 @@ on:
branches: [main, master] branches: [main, master]
workflow_dispatch: workflow_dispatch:
concurrency:
group: stats-${{ github.repository }}
cancel-in-progress: false
# ========================================== # ==========================================
# 🔧 配置区域 - 根据你的项目修改 # 🔧 配置区域 - 根据你的项目修改
# ========================================== # ==========================================
@@ -12,12 +16,17 @@ env:
# ===== Token 配置 ===== # ===== Token 配置 =====
# 请在 Settings -> Secrets 中配置 WORKFLOW secret # 请在 Settings -> Secrets 中配置 WORKFLOW secret
ACCESS_TOKEN: ${{ secrets.WORKFLOW }} ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
GIT_CONFIG_COUNT: "3"
GIT_CONFIG_KEY_0: "credential.helper"
GIT_CONFIG_VALUE_0: ""
GIT_CONFIG_KEY_1: "credential.helper"
GIT_CONFIG_VALUE_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"
# ===== 工作区配置 ===== # ===== 工作区配置 =====
# 持久化 mirror 缓存目录 WORKSPACE_DIR: "/data/workspace"
MIRROR_ROOT: "/data/git-mirrors" WORKSPACE_SLOT: "stats"
# 每个 job 的独立临时工作目录根路径
JOB_WORKSPACE_ROOT: "/home/workspace/jobs"
# ===== 分支配置 ===== # ===== 分支配置 =====
# 徽章数据存储分支(可配置) # 徽章数据存储分支(可配置)
@@ -74,7 +83,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: 🔍 验证 Token 配置 - name: 🔐 验证 Token 配置
id: validate_token id: validate_token
run: | run: |
echo "🔐 验证访问令牌..." echo "🔐 验证访问令牌..."
@@ -159,66 +168,61 @@ jobs:
- name: 📥 准备隔离仓库 - name: 📥 准备隔离仓库
id: clone_main id: clone_main
run: | run: |
set -euo pipefail
echo "======================================" echo "======================================"
echo "🚀 开始准备主仓库" echo "🚀 开始准备主仓库"
echo "======================================" echo "======================================"
REPO_SLUG="${{ github.repository }}" REPO_SLUG="${{ github.repository }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
RUN_ATTEMPT="${{ github.run_attempt }}" TARGET_BRANCH="${{ github.ref_name }}"
JOB_NAME="${{ github.job }}" WORKSPACE_DIR="${{ env.WORKSPACE_DIR }}"
SERVER_HOST="${GITHUB_SERVER_URL#http://}" WORKSPACE_SLOT="${{ env.WORKSPACE_SLOT }}"
SERVER_HOST="${SERVER_HOST#https://}" REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}/${WORKSPACE_SLOT}"
REMOTE_URL="https://oauth2:${{ env.ACCESS_TOKEN }}@${SERVER_HOST}/${REPO_SLUG}.git" REMOTE_URL="${GITHUB_SERVER_URL}/${REPO_SLUG}.git"
BOOTSTRAP_SCRIPT="/tmp/bootstrap_workspace.sh" REMOTE_REF="refs/remotes/origin/${TARGET_BRANCH}"
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 case "$REMOTE_URL" in
RUN_ATTEMPT="1" https://*) ;;
fi *)
echo "❌ 拒绝通过非 HTTPS remote 使用 WORKFLOW token" >&2
if [ -z "$JOB_NAME" ]; then exit 1
JOB_NAME="job" ;;
esac
if [ -e "$REPO_DIR" ] && [ ! -d "$REPO_DIR/.git" ]; then
echo "❌ Slot 已存在但不是 Git 仓库: $REPO_DIR" >&2
exit 1
fi fi
echo "📁 仓库名称: $REPO_NAME" echo "📁 仓库名称: $REPO_NAME"
echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" echo "📦 工作区根目录: $WORKSPACE_DIR"
echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" echo "🎯 Workflow 职责: $WORKSPACE_SLOT"
echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo "🌿 分支: ${{ github.ref_name }}" echo "🌿 分支: $TARGET_BRANCH"
echo "" echo ""
curl -fsSL \ if [ ! -d "$REPO_DIR/.git" ]; then
-H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ mkdir -p "$(dirname "$REPO_DIR")"
"$BOOTSTRAP_URL" \ git clone --no-checkout "$REMOTE_URL" "$REPO_DIR"
-o "$BOOTSTRAP_SCRIPT" else
chmod +x "$BOOTSTRAP_SCRIPT" git -C "$REPO_DIR" remote set-url origin "$REMOTE_URL"
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
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
bash "$BOOTSTRAP_SCRIPT" prepare-job-workspace \
"$REPO_SLUG" \
"$REMOTE_URL" \
"${{ env.MIRROR_ROOT }}" \
"${{ env.JOB_WORKSPACE_ROOT }}" \
"${{ github.run_id }}" \
"$RUN_ATTEMPT" \
"$JOB_NAME" > "$PREPARED_ENV"
# shellcheck source=/dev/null
source "$PREPARED_ENV"
rm -f "$PREPARED_ENV"
echo "✓ Mirror 路径: $MIRROR_PATH"
echo "✓ Job 工作区: $JOB_WORKSPACE"
echo "✓ 仓库目录: $REPO_DIR" echo "✓ 仓库目录: $REPO_DIR"
echo "" echo ""
cd "$REPO_DIR" cd "$REPO_DIR"
# 切换到目标分支
echo "🏷️ 切换到分支: ${{ github.ref_name }}"
git checkout -f ${{ github.ref_name }}
echo "✅ 仓库准备成功" echo "✅ 仓库准备成功"
echo "" echo ""
@@ -235,35 +239,35 @@ jobs:
echo "" echo ""
# 导出环境变量 # 导出环境变量
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 "WORKSPACE_SLOT=$WORKSPACE_SLOT" >> "$GITHUB_ENV"
echo "MIRROR_PATH=$MIRROR_PATH" >> $GITHUB_ENV
echo "BOOTSTRAP_SCRIPT=$BOOTSTRAP_SCRIPT" >> $GITHUB_ENV
echo "✅ 主仓库准备完成" echo "✅ 主仓库准备完成"
echo "======================================" echo "======================================"
echo "" echo ""
- name: ⚙️ 准备统计分支 - name: 🌿 准备统计分支
id: prepare_stats_branch id: prepare_stats_branch
run: | run: |
echo "======================================" echo "======================================"
echo "🔧 准备统计分支" echo "🌿 准备统计分支"
echo "======================================" echo "======================================"
cd "${{ env.REPO_DIR }}" cd "${{ env.REPO_DIR }}"
# 获取所有远程分支 # 获取所有远程分支
git fetch origin --prune git -C "${{ env.REPO_DIR }}" fetch origin --prune
# 检查统计分支是否存在 # 检查统计分支是否存在
if git ls-remote --heads origin ${{ env.BADGE_BRANCH }} | grep -q ${{ env.BADGE_BRANCH }}; then 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 }}' 已存在" echo "✅ 统计分支 '${{ env.BADGE_BRANCH }}' 已存在"
# 检出统计分支 # 检出统计分支
git fetch origin ${{ env.BADGE_BRANCH }}:${{ env.BADGE_BRANCH }} git -C "${{ env.REPO_DIR }}" fetch origin \
git checkout ${{ env.BADGE_BRANCH }} "+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 "📂 当前分支内容:" echo "📂 当前分支内容:"
ls -la ls -la
@@ -274,6 +278,10 @@ jobs:
echo "branch_exists=true" >> $GITHUB_OUTPUT echo "branch_exists=true" >> $GITHUB_OUTPUT
else else
echo "🆕 统计分支 '${{ env.BADGE_BRANCH }}' 不存在,将创建" echo "🆕 统计分支 '${{ env.BADGE_BRANCH }}' 不存在,将创建"
if git show-ref --verify --quiet "refs/heads/${{ env.BADGE_BRANCH }}"; then
echo "🧹 删除上次失败运行遗留的本地统计分支"
git branch -D "${{ env.BADGE_BRANCH }}"
fi
echo "branch_exists=false" >> $GITHUB_OUTPUT echo "branch_exists=false" >> $GITHUB_OUTPUT
fi fi
@@ -383,7 +391,7 @@ jobs:
git commit -m ":bar_chart: chore(stats): 初始化统计分支" git commit -m ":bar_chart: chore(stats): 初始化统计分支"
# 推送到远程 # 推送到远程
git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.BADGE_BRANCH }} git -C "${{ env.REPO_DIR }}" push origin "${{ env.BADGE_BRANCH }}"
echo "✅ 统计分支创建完成" echo "✅ 统计分支创建完成"
echo "======================================" echo "======================================"
@@ -629,9 +637,7 @@ jobs:
echo "📤 推送到远程 ${{ env.BADGE_BRANCH }} 分支..." echo "📤 推送到远程 ${{ env.BADGE_BRANCH }} 分支..."
# 推送到远程(使用 TOKEN) # 推送到远程(使用 TOKEN)
git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.BADGE_BRANCH }} if git -C "${{ env.REPO_DIR }}" push origin "${{ env.BADGE_BRANCH }}"; then
if [ $? -eq 0 ]; then
echo "✅ 推送成功" echo "✅ 推送成功"
echo "changed=true" >> $GITHUB_OUTPUT echo "changed=true" >> $GITHUB_OUTPUT
else else
@@ -643,7 +649,7 @@ jobs:
echo "======================================" echo "======================================"
echo "" echo ""
- name: 📈 生成 Workflow Summary - name: 📋 生成 Workflow Summary
if: env.OUTPUT_TO_SUMMARY == 'true' && success() if: env.OUTPUT_TO_SUMMARY == 'true' && success()
run: | run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY' cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY'
@@ -764,23 +770,9 @@ jobs:
echo " 请检查 LANGUAGE_GROUPS 配置和 MIN_LINES_THRESHOLD 设置" echo " 请检查 LANGUAGE_GROUPS 配置和 MIN_LINES_THRESHOLD 设置"
fi fi
# 清理工作区(可选)
if [ "${{ env.CLEANUP_WORKSPACE }}" == "true" ]; then
echo ""
echo "🧹 清理工作区..."
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
rm -rf "${{ env.JOB_WORKSPACE }}"
fi
echo "✅ 清理完成"
fi
- name: 🧹 清理工作区 - name: 🧹 清理工作区
if: always() if: always()
run: | run: |
echo "🧹 清理临时文件..." echo "🧹 清理临时文件..."
rm -rf /tmp/lang_stats /tmp/lang_summary.txt rm -rf /tmp/lang_stats /tmp/lang_summary.txt
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}"
rm -rf "${{ env.JOB_WORKSPACE }}"
fi
echo "✅ 清理完成" echo "✅ 清理完成"
+6 -5
View File
@@ -20,13 +20,14 @@
## 🧱 运行模型 ## 🧱 运行模型
本模板中的 workflow 采用“Git bare 镜像 + 工作副本”的方案: 本模板中的 workflow 采用“按职责划分固定 slot”的方案:
- Git bare 镜像作为共享仓库缓存,负责复用仓库对象,避免大仓库在每次运行时都重新完整拉取 - 每个会写仓库的 workflow 职责拥有一个固定 slotslot 本身就是 Git clone 根目录,不额外包含 `repo` 子目录
- 每个 job 都在自己的独立工作副本中执行,避免并发任务之间互相污染工作目录 - 第一次运行会 clone 仓库,后续运行通过 fetch、`git reset --hard` 和 clean 重置后复用已有 checkout
- 任务结束后会清理独立工作副本,只保留共享缓存用于后续加速 - 同一 slot 的任务按 concurrency group 串行执行,不同 slot 之间可以并发运行
- 任务结束后保留 slot checkout;长期依赖缓存应放在 checkout 之外,避免被清理命令删除
这个设计的目标是同时兼顾大仓库场景下的同步效率和多 job 并发执行时的隔离性 这个设计适用于仓库和 workflow 职责固定的自托管 CI 服务器,在减少重复 clone 的同时避免多个任务并发修改同一个工作目录
--- ---
+70 -6
View File
@@ -36,6 +36,65 @@ test_stats_workflow_uses_workflow_secret_consistently() {
! rg -q 'GITHUB_TOKEN' "${file}" || fail "stats workflow should not mention GITHUB_TOKEN in token guidance" ! 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() { test_workflow_docs_and_links_use_actual_paths() {
local workflow_doc stats_workflow release_workflow local workflow_doc stats_workflow release_workflow
@@ -61,13 +120,13 @@ test_workflow_doc_describes_workspace_architecture() {
file="${REPO_ROOT}/WORKFLOW.md" file="${REPO_ROOT}/WORKFLOW.md"
grep -Eq '^## .*运行模型' "${file}" || fail "WORKFLOW.md should include a run model section" grep -Eq '^## .*运行模型' "${file}" || fail "WORKFLOW.md should include a run model section"
grep -q 'bare' "${file}" || fail "WORKFLOW.md should describe the bare mirror model" grep -Eq '固定.*slot|slot.*固定' "${file}" || fail "WORKFLOW.md should describe fixed workflow slots"
grep -q '工作副本' "${file}" || fail "WORKFLOW.md should describe isolated work copies" grep -Eq '职责|责任' "${file}" || fail "WORKFLOW.md should describe responsibility ownership"
grep -Eq '共享.*缓存|缓存.*共享' "${file}" || fail "WORKFLOW.md should describe the shared cache model" grep -Eq '并发|串行' "${file}" || fail "WORKFLOW.md should describe slot serialization"
grep -Eq '并发|隔离' "${file}" || fail "WORKFLOW.md should mention concurrency or isolation tradeoffs" grep -Eq 'reset|重置' "${file}" || fail "WORKFLOW.md should describe reset before reuse"
grep -Eq '结束后.*清理|清理.*工作副本' "${file}" || fail "WORKFLOW.md should mention cleanup after workflow completion" 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" ! rg -q 'bootstrap_workspace\.sh' "${file}" || fail "WORKFLOW.md should describe architecture rather than helper implementation"
! rg -q '/data/git-mirrors|/home/workspace/jobs' "${file}" || fail "WORKFLOW.md should avoid implementation-specific workspace paths"
} }
test_presets_do_not_mount_check_crlf_helper() { test_presets_do_not_mount_check_crlf_helper() {
@@ -243,6 +302,11 @@ test_entrypoint_uses_platform_aware_multiarch_verification() {
test_preset_compose_uses_env_for_instance test_preset_compose_uses_env_for_instance
test_workflows_do_not_hardcode_company_server test_workflows_do_not_hardcode_company_server
test_stats_workflow_uses_workflow_secret_consistently 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_docs_and_links_use_actual_paths
test_workflow_doc_describes_workspace_architecture test_workflow_doc_describes_workspace_architecture
test_presets_do_not_mount_check_crlf_helper test_presets_do_not_mount_check_crlf_helper
-169
View File
@@ -1,169 +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}/.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_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 alternates_file alternates_target
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"
alternates_file="${repo_dir}/.git/objects/info/alternates"
if [ ! -f "${alternates_file}" ]; then
echo "FAIL: prepared repo should use shared mirror objects via alternates" >&2
exit 1
fi
alternates_target=$(cat "${alternates_file}")
assert_eq "${mirror_path}/objects" "${alternates_target}" "prepared repo should borrow objects from the bare mirror"
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_repo_path_layout
test_job_identity_prefers_run_metadata
test_sanitize_job_name
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
echo "workspace_helper_test.sh: PASS"