diff --git a/.gitea/ci/bootstrap_workspace.sh b/.gitea/ci/bootstrap_workspace.sh new file mode 100644 index 0000000..f4b2040 --- /dev/null +++ b/.gitea/ci/bootstrap_workspace.sh @@ -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 + bootstrap_workspace.sh cleanup-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 diff --git a/.gitea/workflows/changelog_and_release.yml b/.gitea/workflows/changelog_and_release.yml index 948119b..b802fa6 100644 --- a/.gitea/workflows/changelog_and_release.yml +++ b/.gitea/workflows/changelog_and_release.yml @@ -31,8 +31,10 @@ env: ACCESS_TOKEN: ${{ secrets.WORKFLOW }} # ===== 工作区配置 ===== - # 完整克隆的工作目录 - 自建的runner可以复用工作区 - WORKSPACE_DIR: "/home/workspace" + # 持久化 mirror 缓存目录 + MIRROR_ROOT: "/data/git-mirrors" + # 每个 job 的独立临时工作目录根路径 + JOB_WORKSPACE_ROOT: "/home/workspace/jobs" # ===== 分支配置 ===== # 主分支名称(用于推送 CHANGELOG 更新) @@ -228,87 +230,64 @@ jobs: echo "======================================" echo "" - - name: 📥 克隆仓库 + - name: 📥 准备隔离仓库 id: clone run: | echo "======================================" echo "🚀 开始准备仓库" echo "======================================" + REPO_SLUG="${{ github.repository }}" 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_DIR" + echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" + echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "" - # 检查仓库状态 - if [ -d "$REPO_DIR" ]; then - echo "📂 目录已存在,检查 Git 仓库状态..." + curl -fsSL \ + -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ + "$BOOTSTRAP_URL" \ + -o "$BOOTSTRAP_SCRIPT" + chmod +x "$BOOTSTRAP_SCRIPT" - if [ -d "$REPO_DIR/.git" ]; then - # 目录存在且是有效的 Git 仓库 - echo "✓ 发现有效的 Git 仓库,执行增量更新..." - cd "$REPO_DIR" + 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" - # 清理工作区 - git clean -fdx - git reset --hard + # shellcheck source=/dev/null + source "$PREPARED_ENV" + rm -f "$PREPARED_ENV" - # 获取最新代码和标签,同时清理远程已删除的 tag - echo "📥 拉取最新代码和标签..." - git fetch --all --tags --force --prune --prune-tags - echo "✓ 已同步远程状态(包括已删除的 tag)" + echo "✓ Mirror 路径: $MIRROR_PATH" + echo "✓ Job 工作区: $JOB_WORKSPACE" + 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 "❌ 克隆失败" - cat /tmp/git_clone.log - - # 清理残留 - if [ -d "$REPO_DIR" ]; then - rm -rf "$REPO_DIR" - fi - - exit 1 - fi - - 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 + cd "$REPO_DIR" echo "" echo "🔍 验证主分支配置..." @@ -325,7 +304,6 @@ jobs: exit 1 fi - MAIN_BRANCH="${{ env.MAIN_BRANCH }}" echo "✓ 使用配置的主分支: $MAIN_BRANCH" # 验证分支是否存在 @@ -351,7 +329,10 @@ jobs: # 导出到环境变量供后续步骤使用 echo "REPO_DIR=$REPO_DIR" >> $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 "BOOTSTRAP_SCRIPT=$BOOTSTRAP_SCRIPT" >> $GITHUB_ENV echo "" echo "✅ 仓库准备完成" @@ -1078,16 +1059,6 @@ jobs: 检测到此提交由 Bot 创建(包含 `[skip ci]` 标记),为防止无限循环,已跳过执行。 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 if [ "${{ steps.changelog.outputs.content_changed }}" = "true" ]; then cat >> $GITHUB_STEP_SUMMARY << 'EOFSUCCESS' @@ -1175,9 +1146,6 @@ jobs: if [ "${{ steps.check_bot.outputs.is_bot_commit }}" = "true" ]; then echo "⏭️ 已跳过: Bot 提交检测" 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 echo "📊 执行结果:" echo " - Tag: ${{ github.ref_name }}" @@ -1223,4 +1191,8 @@ jobs: run: | 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 + if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then + echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}" + rm -rf "${{ env.JOB_WORKSPACE }}" + fi echo "✅ 清理完成" diff --git a/.gitea/workflows/update_stats_badge.yaml b/.gitea/workflows/update_stats_badge.yaml index a743cda..defebfc 100644 --- a/.gitea/workflows/update_stats_badge.yaml +++ b/.gitea/workflows/update_stats_badge.yaml @@ -14,8 +14,10 @@ env: ACCESS_TOKEN: ${{ secrets.WORKFLOW }} # ===== 工作区配置 ===== - # 完整克隆的工作目录 - WORKSPACE_DIR: "/home/workspace" + # 持久化 mirror 缓存目录 + MIRROR_ROOT: "/data/git-mirrors" + # 每个 job 的独立临时工作目录根路径 + JOB_WORKSPACE_ROOT: "/home/workspace/jobs" # ===== 分支配置 ===== # 徽章数据存储分支(可配置) @@ -177,80 +179,64 @@ jobs: echo "======================================" echo "" - - name: 📥 克隆主仓库 + - name: 📥 准备隔离仓库 id: clone_main run: | echo "======================================" echo "🚀 开始准备主仓库" echo "======================================" + REPO_SLUG="${{ github.repository }}" 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_DIR" + echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" + echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "🌿 分支: ${{ github.ref_name }}" echo "" - # 检查仓库状态 - if [ -d "$REPO_DIR" ]; then - echo "📂 目录已存在,检查 Git 仓库状态..." + curl -fsSL \ + -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ + "$BOOTSTRAP_URL" \ + -o "$BOOTSTRAP_SCRIPT" + chmod +x "$BOOTSTRAP_SCRIPT" - if [ -d "$REPO_DIR/.git" ]; then - # 目录存在且是有效的 Git 仓库 - echo "✓ 发现有效的 Git 仓库,执行增量更新..." - cd "$REPO_DIR" + 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" - # 清理工作区 - git clean -fdx - git reset --hard + # shellcheck source=/dev/null + source "$PREPARED_ENV" + rm -f "$PREPARED_ENV" - # 获取最新代码 - echo "📥 拉取最新代码..." - git fetch --all --tags --force + echo "✓ Mirror 路径: $MIRROR_PATH" + echo "✓ Job 工作区: $JOB_WORKSPACE" + 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" - 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 + cd "$REPO_DIR" # 切换到目标分支 echo "🏷️ 切换到分支: ${{ github.ref_name }}" @@ -274,6 +260,9 @@ jobs: # 导出环境变量 echo "REPO_DIR=$REPO_DIR" >> $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 "======================================" @@ -964,7 +953,9 @@ jobs: if [ "${{ env.CLEANUP_WORKSPACE }}" == "true" ]; then echo "" echo "🧹 清理工作区..." - rm -rf "${{ env.WORKSPACE_DIR }}" + if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then + rm -rf "${{ env.JOB_WORKSPACE }}" + fi echo "✅ 清理完成" fi @@ -973,4 +964,8 @@ jobs: run: | echo "🧹 清理临时文件..." 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 "✅ 清理完成" diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 5414b78..c004ee9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -33,6 +33,9 @@ docker-runner/ │ ├── register.sh # Runner 注册脚本 │ └── manage.sh # Runner 管理脚本 │ +├── .gitea/ci/ +│ └── bootstrap_workspace.sh # workflow 自举脚本(下载后准备 mirror 和独立工作区) +│ └── presets/ # 预设配置(选择一个使用) ├── standard-ubuntu-22/ # 标准版 (Ubuntu 22.04) │ ├── Dockerfile @@ -49,7 +52,14 @@ docker-runner/ - `common/` 目录中的脚本由所有版本共享,通过 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//.git` +- 每个 job 会从 mirror 本地克隆到独立目录 `/home/workspace/jobs////repo` +- job 结束后临时工作目录会自动清理,mirror 会保留以加速后续大仓库同步 --- @@ -139,7 +149,7 @@ docker compose exec gitea-runner /data/register.sh 输入你的 Gitea 实例 URL 和注册令牌。 -Runner 注册后会自动启动,无需重启容器。 +Runner 注册后会自动启动,无需重启容器。新注册的 runner 默认并发为 `4`。 #### 6. 验证运行状态 @@ -577,6 +587,9 @@ docker system df runner-data/ ├── bin/ │ └── act_runner # Runner 可执行文件(持久化) +├── git-mirrors/ # 持久化 bare mirror 缓存 +│ └── / +│ └── .git ├── runners/ # Runner 配置目录 │ └── / │ ├── .runner # 注册信息 @@ -586,7 +599,7 @@ runner-data/ └── .configured ``` -容器重启或重建后,数据不会丢失。 +容器重启或重建后,数据不会丢失。workflow 的临时工作目录位于 `/home/workspace/jobs/...`,任务结束后会自动清理,不会作为持久化数据保留。 ### Q10: 如何切换不同预设? diff --git a/README.md b/README.md index b6763aa..3b71eaa 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ - 📚 **文档规范**:统一的文档格式和版本管理规范 - 🔧 **配置指南**:详细的配置说明和最佳实践 +## ⚙️ 当前默认行为 + +- 新注册的 runner 默认 `capacity=4` +- 大仓库会缓存到 `/data/git-mirrors//.git` +- 每个 workflow job 使用独立临时目录 `/home/workspace/jobs////repo` +- job 结束后会自动清理临时工作目录,mirror 缓存保留在 `runner-data/` + ## 📂 文档导航 ### 🚀 Runner diff --git a/docker-runner/common/register.sh b/docker-runner/common/register.sh index 99b4408..6005db3 100644 --- a/docker-runner/common/register.sh +++ b/docker-runner/common/register.sh @@ -161,7 +161,7 @@ try: # 使用实际注册的 labels config['runner']['labels'] = registered_labels - config['runner']['capacity'] = 2 + config['runner']['capacity'] = 4 # 启用缓存 if 'cache' not in config: @@ -175,7 +175,7 @@ try: print("✓ Configuration updated using Python") print(f" - Labels: {registered_labels}") - print(f" - Capacity: 2") + print(f" - Capacity: 4") print(f" - Cache enabled: ./cache") sys.exit(0) @@ -191,7 +191,7 @@ PYEOF echo "⚠ Python configuration failed, using basic sed..." # 基本的 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|dir: ""|dir: ./cache|g' config.yaml || true @@ -202,7 +202,7 @@ else echo "⚠ Python3 not found, applying basic configuration..." # 基本的 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|dir: ""|dir: ./cache|g' config.yaml || true diff --git a/tests/workspace_helper_test.sh b/tests/workspace_helper_test.sh new file mode 100644 index 0000000..0518657 --- /dev/null +++ b/tests/workspace_helper_test.sh @@ -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"