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