#!/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 } 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"