diff --git a/.gitea/ci/bootstrap_workspace.sh b/.gitea/ci/bootstrap_workspace.sh deleted file mode 100644 index 7fc81c8..0000000 --- a/.gitea/ci/bootstrap_workspace.sh +++ /dev/null @@ -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 - 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 491d70f..1357a09 100644 --- a/.gitea/workflows/changelog_and_release.yml +++ b/.gitea/workflows/changelog_and_release.yml @@ -29,16 +29,21 @@ env: # - GIT_TOKEN: 用于 git clone/push # - API_TOKEN: 用于 API 调用 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 缓存目录 - MIRROR_ROOT: "/data/git-mirrors" - # 每个 job 的独立临时工作目录根路径 - JOB_WORKSPACE_ROOT: "/home/workspace/jobs" + WORKSPACE_DIR: "/data/workspace" + WORKSPACE_SLOT: "release" # ===== 分支配置 ===== # 主分支名称(用于推送 CHANGELOG 更新) - # 如果留空,会自动检测(main 或 master) + # 必须明确配置主分支(例如 main 或 master) MAIN_BRANCH: "main" # ===== 服务器配置 ===== @@ -233,6 +238,7 @@ jobs: - name: 📥 准备隔离仓库 id: clone run: | + set -euo pipefail echo "======================================" echo "🚀 开始准备仓库" echo "======================================" @@ -240,106 +246,69 @@ jobs: REPO_SLUG="${{ github.repository }}" REPO_NAME="${{ github.event.repository.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) + 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}" - if [ -z "$RUN_ATTEMPT" ]; then - RUN_ATTEMPT="1" + if [ -z "$MAIN_BRANCH" ]; then + echo "❌ 错误: 未配置主分支" + exit 1 fi - - if [ -z "$JOB_NAME" ]; then - JOB_NAME="job" + case "$REMOTE_URL" in + https://*) ;; + *) + 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 echo "📁 仓库名称: $REPO_NAME" - echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" - echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" + echo "📦 工作区根目录: $WORKSPACE_DIR" + echo "🎯 Workflow 职责: $WORKSPACE_SLOT" echo "🌐 服务器: ${GITHUB_SERVER_URL}" echo "" - curl -fsSL \ - -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ - "$BOOTSTRAP_URL" \ - -o "$BOOTSTRAP_SCRIPT" - chmod +x "$BOOTSTRAP_SCRIPT" + 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" + 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 "" cd "$REPO_DIR" - - 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 "✓ 主分支 '$MAIN_BRANCH' 已准备并清理" # 导出到环境变量供后续步骤使用 - 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 "REPO_DIR=$REPO_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 "" - - name: 🔍 检查 Bot 提交 + - name: 🤖 检查 Bot 提交 id: check_bot run: | echo "======================================" @@ -829,9 +798,7 @@ jobs: git commit -m "${{ env.COMMIT_MSG }}" echo "📤 推送到远程 ${{ env.MAIN_BRANCH }} 分支..." - git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.MAIN_BRANCH }} - - if [ $? -eq 0 ]; then + if git -C "${{ env.REPO_DIR }}" push origin "${{ env.MAIN_BRANCH }}"; then echo "✅ 推送成功" else echo "❌ 推送失败" @@ -853,7 +820,7 @@ jobs: cd ${{ env.REPO_DIR }} # 确保使用最新的 CHANGELOG - git pull origin ${{ env.MAIN_BRANCH }} + git -C "${{ env.REPO_DIR }}" pull --ff-only origin "${{ env.MAIN_BRANCH }}" python3 << 'PYSCRIPT' import re @@ -1029,11 +996,11 @@ jobs: echo "======================================" echo "" - - name: 📊 生成 Workflow Summary + - name: 📋 生成 Workflow Summary if: always() run: | echo "======================================" - echo "📊 生成 Workflow Summary" + echo "📋 生成 Workflow Summary" echo "======================================" cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY' @@ -1071,7 +1038,6 @@ jobs: - ✅ CHANGELOG.md 已更新 - ✅ 更改已推送到主分支 (`${{ env.MAIN_BRANCH }}`) - ✅ Release 已创建 - - ✅ CHANGELOG.md 已作为附件上传 EOFSUCCESS else cat >> $GITHUB_STEP_SUMMARY << 'EOFREBUILD' @@ -1083,7 +1049,6 @@ jobs: - ℹ️ CHANGELOG.md 无需更新(内容已存在) - ✅ Release 已创建 - - ✅ CHANGELOG.md 已作为附件上传 💡 **说明**: 检测到这是重建已删除的 tag,CHANGELOG 中已包含所有提交内容,因此直接从现有内容创建 Release。 EOFREBUILD @@ -1159,11 +1124,9 @@ jobs: echo "✅ 已完成的操作:" echo " - CHANGELOG.md 已更新并推送到 ${{ env.MAIN_BRANCH }} 分支" echo " - Release 已创建" - echo " - CHANGELOG.md 已上传为附件" else echo "✅ 已完成的操作:" echo " - Release 已创建(从现有 CHANGELOG 内容)" - echo " - CHANGELOG.md 已上传为附件" echo "" echo "💡 说明: 检测到这是重建已删除的 tag,CHANGELOG 中已包含" echo " 所有提交内容,因此直接从现有内容创建 Release。" @@ -1191,8 +1154,4 @@ 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/ubuntu_system_info.yml b/.gitea/workflows/ubuntu_system_info.yml index 1b3fbf6..770a19e 100644 --- a/.gitea/workflows/ubuntu_system_info.yml +++ b/.gitea/workflows/ubuntu_system_info.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-22.04 steps: - - name: 打印问候 + - name: 👋 打印问候 run: | echo "=====================================" echo " Ubuntu System Information Check " echo "=====================================" echo "" - - name: 显示系统信息 + - name: 🖥️ 显示系统信息 run: | echo "===== 系统版本 =====" cat /etc/os-release @@ -32,7 +32,7 @@ jobs: echo "===== 系统启动时间 =====" uptime - - name: 显示 CPU 信息 + - name: 🧮 显示 CPU 信息 run: | echo "===== CPU 详细信息 =====" lscpu @@ -43,7 +43,7 @@ jobs: echo "===== CPU 核心数 =====" nproc - - name: 显示内存信息 + - name: 🧠 显示内存信息 run: | echo "===== 内存使用情况 =====" free -h @@ -51,7 +51,7 @@ jobs: echo "===== 内存详细信息 =====" cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable' - - name: 显示磁盘信息 + - name: 💾 显示磁盘信息 run: | echo "===== 磁盘使用情况 =====" df -h @@ -59,7 +59,7 @@ jobs: echo "===== 磁盘分区信息 =====" lsblk - - name: 显示网络信息 + - name: 🌐 显示网络信息 run: | echo "===== 网络接口 =====" cat /proc/net/dev @@ -70,7 +70,7 @@ jobs: echo "===== DNS 配置 =====" cat /etc/resolv.conf - - name: 显示已安装的软件 + - name: 📦 显示已安装的软件 run: | echo "===== Python 版本 =====" python3 --version @@ -84,7 +84,7 @@ jobs: echo "===== Git 版本 =====" git --version - - name: 显示环境变量 + - name: 🔧 显示环境变量 run: | echo "===== 重要环境变量 =====" echo "PATH: $PATH" @@ -93,7 +93,7 @@ jobs: echo "SHELL: $SHELL" echo "PWD: $PWD" - - name: 显示当前目录 + - name: 📂 显示当前目录 run: | echo "===== 当前工作目录 =====" pwd diff --git a/.gitea/workflows/update_stats_badge.yaml b/.gitea/workflows/update_stats_badge.yaml index 8dc032d..711d190 100644 --- a/.gitea/workflows/update_stats_badge.yaml +++ b/.gitea/workflows/update_stats_badge.yaml @@ -5,6 +5,10 @@ on: branches: [main, master] workflow_dispatch: +concurrency: + group: stats-${{ github.repository }} + cancel-in-progress: false + # ========================================== # 🔧 配置区域 - 根据你的项目修改 # ========================================== @@ -12,12 +16,17 @@ env: # ===== Token 配置 ===== # 请在 Settings -> Secrets 中配置 WORKFLOW secret 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 缓存目录 - MIRROR_ROOT: "/data/git-mirrors" - # 每个 job 的独立临时工作目录根路径 - JOB_WORKSPACE_ROOT: "/home/workspace/jobs" + WORKSPACE_DIR: "/data/workspace" + WORKSPACE_SLOT: "stats" # ===== 分支配置 ===== # 徽章数据存储分支(可配置) @@ -74,7 +83,7 @@ jobs: contents: write steps: - - name: 🔍 验证 Token 配置 + - name: 🔐 验证 Token 配置 id: validate_token run: | echo "🔐 验证访问令牌..." @@ -159,66 +168,61 @@ jobs: - name: 📥 准备隔离仓库 id: clone_main run: | + set -euo pipefail echo "======================================" echo "🚀 开始准备主仓库" echo "======================================" REPO_SLUG="${{ github.repository }}" REPO_NAME="${{ github.event.repository.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) + 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}" - if [ -z "$RUN_ATTEMPT" ]; then - RUN_ATTEMPT="1" - fi - - if [ -z "$JOB_NAME" ]; then - JOB_NAME="job" + case "$REMOTE_URL" in + https://*) ;; + *) + 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 echo "📁 仓库名称: $REPO_NAME" - echo "🪞 Mirror 根目录: ${{ env.MIRROR_ROOT }}" - echo "📦 Job 工作区根目录: ${{ env.JOB_WORKSPACE_ROOT }}" + echo "📦 工作区根目录: $WORKSPACE_DIR" + echo "🎯 Workflow 职责: $WORKSPACE_SLOT" echo "🌐 服务器: ${GITHUB_SERVER_URL}" - echo "🌿 分支: ${{ github.ref_name }}" + echo "🌿 分支: $TARGET_BRANCH" echo "" - curl -fsSL \ - -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ - "$BOOTSTRAP_URL" \ - -o "$BOOTSTRAP_SCRIPT" - chmod +x "$BOOTSTRAP_SCRIPT" + 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" + 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 "" cd "$REPO_DIR" - # 切换到目标分支 - echo "🏷️ 切换到分支: ${{ github.ref_name }}" - git checkout -f ${{ github.ref_name }} - echo "✅ 仓库准备成功" echo "" @@ -235,35 +239,35 @@ jobs: echo "" # 导出环境变量 - 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 "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV" + echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV" + echo "WORKSPACE_SLOT=$WORKSPACE_SLOT" >> "$GITHUB_ENV" echo "✅ 主仓库准备完成" echo "======================================" echo "" - - name: ⚙️ 准备统计分支 + - name: 🌿 准备统计分支 id: prepare_stats_branch run: | echo "======================================" - echo "🔧 准备统计分支" + echo "🌿 准备统计分支" echo "======================================" 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 }}' 已存在" # 检出统计分支 - git fetch origin ${{ env.BADGE_BRANCH }}:${{ env.BADGE_BRANCH }} - git checkout ${{ 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 @@ -274,6 +278,10 @@ jobs: 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 }}" + fi echo "branch_exists=false" >> $GITHUB_OUTPUT fi @@ -383,7 +391,7 @@ jobs: 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 "======================================" @@ -629,9 +637,7 @@ jobs: echo "📤 推送到远程 ${{ env.BADGE_BRANCH }} 分支..." # 推送到远程(使用 TOKEN) - git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.BADGE_BRANCH }} - - if [ $? -eq 0 ]; then + if git -C "${{ env.REPO_DIR }}" push origin "${{ env.BADGE_BRANCH }}"; then echo "✅ 推送成功" echo "changed=true" >> $GITHUB_OUTPUT else @@ -643,7 +649,7 @@ jobs: echo "======================================" echo "" - - name: 📈 生成 Workflow Summary + - name: 📋 生成 Workflow Summary if: env.OUTPUT_TO_SUMMARY == 'true' && success() run: | cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY' @@ -764,23 +770,9 @@ jobs: echo " 请检查 LANGUAGE_GROUPS 配置和 MIN_LINES_THRESHOLD 设置" 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: 🧹 清理工作区 if: always() run: | echo "🧹 清理临时文件..." 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 "✅ 清理完成" diff --git a/WORKFLOW.md b/WORKFLOW.md index 9b55770..c715e24 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -20,13 +20,14 @@ ## 🧱 运行模型 -本模板中的 workflow 采用“Git bare 镜像 + 工作副本”的方案: +本模板中的 workflow 采用“按职责划分固定 slot”的方案: -- Git bare 镜像作为共享仓库缓存,负责复用仓库对象,避免大仓库在每次运行时都重新完整拉取 -- 每个 job 都在自己的独立工作副本中执行,避免并发任务之间互相污染工作目录 -- 任务结束后会清理独立工作副本,只保留共享缓存用于后续加速 +- 每个会写仓库的 workflow 职责拥有一个固定 slot,slot 本身就是 Git clone 根目录,不额外包含 `repo` 子目录 +- 第一次运行会 clone 仓库,后续运行通过 fetch、`git reset --hard` 和 clean 重置后复用已有 checkout +- 同一 slot 的任务按 concurrency group 串行执行,不同 slot 之间可以并发运行 +- 任务结束后保留 slot checkout;长期依赖缓存应放在 checkout 之外,避免被清理命令删除 -这个设计的目标是同时兼顾大仓库场景下的同步效率和多 job 并发执行时的隔离性。 +这个设计适用于仓库和 workflow 职责固定的自托管 CI 服务器,在减少重复 clone 的同时避免多个任务并发修改同一个工作目录。 --- diff --git a/tests/template_defaults_test.sh b/tests/template_defaults_test.sh index 2bcb106..d27d60a 100644 --- a/tests/template_defaults_test.sh +++ b/tests/template_defaults_test.sh @@ -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" } +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 @@ -61,13 +120,13 @@ test_workflow_doc_describes_workspace_architecture() { file="${REPO_ROOT}/WORKFLOW.md" 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 -q '工作副本' "${file}" || fail "WORKFLOW.md should describe isolated work copies" - grep -Eq '共享.*缓存|缓存.*共享' "${file}" || fail "WORKFLOW.md should describe the shared cache model" - grep -Eq '并发|隔离' "${file}" || fail "WORKFLOW.md should mention concurrency or isolation tradeoffs" - grep -Eq '结束后.*清理|清理.*工作副本' "${file}" || fail "WORKFLOW.md should mention cleanup after workflow completion" + 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" - ! 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() { @@ -243,6 +302,11 @@ test_entrypoint_uses_platform_aware_multiarch_verification() { 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 diff --git a/tests/workspace_helper_test.sh b/tests/workspace_helper_test.sh deleted file mode 100644 index b14cc8f..0000000 --- a/tests/workspace_helper_test.sh +++ /dev/null @@ -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"