Compare commits

...

6 Commits

Author SHA1 Message Date
csh 1259495cbb 🐛 fix(stats): switch badges to self-hosted svg
Ubuntu System Information / show-system-info (push) Successful in 0s Details
2026-05-22 15:45:24 +08:00
csh d4fa498a4a 📝 docs(readme): align document navigation headings 2026-05-22 14:16:06 +08:00
csh 8445c6a363 🐛 fix(buildx_runner): verify multiarch with explicit platforms 2026-05-22 14:05:19 +08:00
csh ebd0f94053 ♻️ refactor(runner): simplify shell helpers and workflow scripts 2026-05-22 13:50:36 +08:00
csh 40fa747a94 🔧 chore(runner): allow preset identity overrides via env 2026-05-22 13:19:09 +08:00
csh e5db752c55 🐛 fix(docker_runner): validate act_runner binary arch 2026-05-22 11:58:01 +08:00
21 changed files with 701 additions and 355 deletions

View File

@ -50,14 +50,6 @@ build_mirror_path() {
printf '%s/%s/%s.git\n' "${mirror_root}" "${owner}" "${repo_name}" printf '%s/%s/%s.git\n' "${mirror_root}" "${owner}" "${repo_name}"
} }
build_mirror_lock_path() {
local mirror_root=$1
local owner=$2
local repo_name=$3
printf '%s.lock\n' "$(build_mirror_path "${mirror_root}" "${owner}" "${repo_name}")"
}
build_job_workspace_root() { build_job_workspace_root() {
local workspace_root=$1 local workspace_root=$1
local owner=$2 local owner=$2

View File

@ -0,0 +1,243 @@
#!/usr/bin/env python3
import argparse
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import List
LABEL_BG = "#555555"
DEFAULT_TOTAL_COLOR = "#1f6feb"
DEFAULT_FILES_COLOR = "#2da44e"
DEFAULT_LANGUAGE_COUNT_COLOR = "#8250df"
TEXT_COLOR = "#ffffff"
@dataclass
class LanguageStat:
lang_id: str
display_name: str
code_lines: int
formatted_lines: str
file_count: int
color: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", required=True)
parser.add_argument("--readme-path", required=True)
parser.add_argument("--repo", required=True)
parser.add_argument("--generated-by", required=True)
parser.add_argument("--updated-at", required=True)
parser.add_argument("--total-color", default=DEFAULT_TOTAL_COLOR)
parser.add_argument("--files-color", default=DEFAULT_FILES_COLOR)
parser.add_argument("--language-count-color", default=DEFAULT_LANGUAGE_COUNT_COLOR)
parser.add_argument("--total-code", required=True, type=int)
parser.add_argument("--formatted-total-code", required=True)
parser.add_argument("--total-files", required=True, type=int)
parser.add_argument("--formatted-total-files", required=True)
parser.add_argument("--language-count", required=True, type=int)
parser.add_argument("--exclude-dirs", required=True)
parser.add_argument("--min-lines-threshold", required=True, type=int)
parser.add_argument("--lang-summary", required=True)
return parser.parse_args()
def escape_xml(value: str) -> str:
return (
value.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
.replace("'", "&apos;")
)
def display_units(value: str) -> int:
units = 0
for char in value:
if unicodedata.east_asian_width(char) in {"F", "W"}:
units += 2
else:
units += 1
return units
def text_width(value: str) -> int:
return display_units(value) * 7 + 20
def normalize_color(value: str, fallback: str) -> str:
if not value:
return fallback
return value if value.startswith("#") else f"#{value}"
def parse_language_summary(path: Path) -> List[LanguageStat]:
stats: List[LanguageStat] = []
if not path.exists():
return stats
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line:
continue
parts = line.split("|")
if len(parts) < 6:
continue
while len(parts) < 7:
parts.append("")
lang_id, display_name, code_lines, formatted_lines, file_count, color, _icon = parts[:7]
stats.append(
LanguageStat(
lang_id=lang_id,
display_name=display_name,
code_lines=int(code_lines),
formatted_lines=formatted_lines,
file_count=int(file_count),
color=normalize_color(color, DEFAULT_TOTAL_COLOR),
)
)
return stats
def badge_svg(label: str, message: str, color: str) -> str:
height = 20
left_width = max(46, text_width(label))
right_width = max(46, text_width(message))
total_width = left_width + right_width
left_text_x = left_width / 2
right_text_x = left_width + right_width / 2
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{total_width}" height="{height}" role="img" aria-label="{escape_xml(label)}: {escape_xml(message)}">
<title>{escape_xml(label)}: {escape_xml(message)}</title>
<rect width="{left_width}" height="{height}" fill="{LABEL_BG}" />
<rect x="{left_width}" width="{right_width}" height="{height}" fill="{escape_xml(color)}" />
<rect width="{total_width}" height="{height}" rx="3" fill="transparent" />
<g fill="{TEXT_COLOR}" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="{left_text_x:.1f}" y="14">{escape_xml(label)}</text>
<text x="{right_text_x:.1f}" y="14">{escape_xml(message)}</text>
</g>
</svg>
"""
def write_badge(path: Path, label: str, message: str, color: str) -> None:
path.write_text(badge_svg(label, message, color), encoding="utf-8")
def render_readme(args: argparse.Namespace, languages: List[LanguageStat], badge_dir_name: str) -> str:
language_rows = []
for stat in languages:
share = (stat.code_lines * 100 / args.total_code) if args.total_code else 0.0
language_rows.append(
f"| **{stat.display_name}** | {stat.formatted_lines} | {stat.file_count} | {share:.1f}% | "
f"![{stat.display_name}](./{badge_dir_name}/{stat.lang_id}-lines.svg) |"
)
if language_rows:
language_table = "\n".join(language_rows)
else:
language_table = "| _未找到符合条件的语言_ | - | - | - | - |"
return f"""# 📊 代码统计详细报告
> 此分支由 {args.generated_by} 自动生成和维护
>
> **请勿手动修改此分支的内容**
>
> 🤖 {args.generated_by} 自动生成
>
> 📅 更新时间: {args.updated_at}
![总代码](./{badge_dir_name}/total-lines.svg)
![总文件](./{badge_dir_name}/total-files.svg)
![语言种类](./{badge_dir_name}/language-count.svg)
## 📈 总体统计
| 统计项 | 数值 | 徽章 |
|--------|------|------|
| 💻 总代码行数 | **{args.formatted_total_code}** | ![总代码](./{badge_dir_name}/total-lines.svg) |
| 📁 总文件数 | **{args.formatted_total_files}** | ![总文件](./{badge_dir_name}/total-files.svg) |
| 🌐 语言种类 | **{args.language_count}** | ![语言种类](./{badge_dir_name}/language-count.svg) |
## 🌈 语言分布
| 语言 | 代码行数 | 文件数 | 占比 | 徽章 |
|------|----------|--------|------|------|
{language_table}
---
## ⚙️ 配置说明
### Token 配置
- 当前使用: **WORKFLOW**
- 需要在 Settings -> Actions -> Secrets 中配置 `WORKFLOW`
### 排除规则
当前排除的目录:
```
{args.exclude_dirs}
```
### 阈值设置
- 最小代码行数阈值: **{args.min_lines_threshold}**
- 低于此阈值的语言不会生成徽章
---
*📊 统计仓库`{args.repo}`*
"""
def main() -> None:
args = parse_args()
output_dir = Path(args.output_dir)
readme_path = Path(args.readme_path)
output_dir.mkdir(parents=True, exist_ok=True)
readme_path.parent.mkdir(parents=True, exist_ok=True)
languages = parse_language_summary(Path(args.lang_summary))
badge_dir_name = output_dir.name
write_badge(
output_dir / "total-lines.svg",
"代码",
f"{args.formatted_total_code}",
normalize_color(args.total_color, DEFAULT_TOTAL_COLOR),
)
write_badge(
output_dir / "total-files.svg",
"文件",
f"{args.formatted_total_files}",
normalize_color(args.files_color, DEFAULT_FILES_COLOR),
)
write_badge(
output_dir / "language-count.svg",
"语言",
f"{args.language_count}",
normalize_color(args.language_count_color, DEFAULT_LANGUAGE_COUNT_COLOR),
)
for stat in languages:
write_badge(output_dir / f"{stat.lang_id}-lines.svg", stat.display_name, f"{stat.formatted_lines}", stat.color)
readme_path.write_text(render_readme(args, languages, badge_dir_name), encoding="utf-8")
if __name__ == "__main__":
main()

View File

@ -35,17 +35,12 @@ env:
SPECIAL_INCLUDES: "" SPECIAL_INCLUDES: ""
# 示例: 'dist:js,css|vendor:go,mod' # 示例: 'dist:js,css|vendor:go,mod'
# ===== 徽章颜色配置 ===== # ===== SVG 徽章颜色配置 =====
COLOR_TOTAL: "blue" COLOR_TOTAL: "blue"
COLOR_FILES: "green" COLOR_FILES: "green"
COLOR_DEFAULT: "brightgreen" COLOR_LANGUAGE_COUNT: "purple"
# ===== 徽章样式配置 =====
BADGE_STYLE: "flat" # 可选: flat, flat-square, plastic, for-the-badge, social
# ===== 输出配置 ===== # ===== 输出配置 =====
# 是否生成详细报告
GENERATE_DETAILED_REPORT: "true"
# 是否输出到 workflow summary # 是否输出到 workflow summary
OUTPUT_TO_SUMMARY: "true" OUTPUT_TO_SUMMARY: "true"
# 最小代码行数阈值(低于此值的语言不生成徽章) # 最小代码行数阈值(低于此值的语言不生成徽章)
@ -55,18 +50,6 @@ env:
GIT_USER_NAME: "ci[bot]" GIT_USER_NAME: "ci[bot]"
GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn" GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn"
# ===== 平台配置 =====
# 平台类型: github 或 gitea
PLATFORM: "gitea"
# Git 服务器 URLGitea 示例: https://gitea.example.com
GIT_SERVER_URL: "${{ github.server_url }}"
# 仓库路径(格式: owner/repo
REPO_PATH: "${{ github.repository }}"
# Raw 文件基础 URL
# GitHub: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
# Gitea: https://gitea.example.com/{owner}/{repo}/raw/branch/{branch}/{path}
RAW_URL_BASE: '${{ github.server_url }}/${{ github.repository }}/raw/branch'
# ========================================== # ==========================================
# 🎨 语言分组配置 # 🎨 语言分组配置
# 格式: 组名:后缀列表:显示名称:颜色:图标(可选) # 格式: 组名:后缀列表:显示名称:颜色:图标(可选)
@ -363,19 +346,19 @@ jobs:
cat > README.md << 'EOF' cat > README.md << 'EOF'
# 📊 代码统计徽章数据 # 📊 代码统计徽章数据
> 此分支由 GitHub Actions 自动生成和维护 > 此分支由 Gitea Actions 自动生成和维护
> >
> ⚠️ **请勿手动修改此分支的内容!** > ⚠️ **请勿手动修改此分支的内容!**
## 📁 目录结构 ## 📁 目录结构
``` ```
README.md # 详细统计报告
badges/ badges/
├── total-lines.json # 总代码行数徽章 ├── total-lines.svg # 总代码行数徽章
├── total-files.json # 总文件数徽章 ├── total-files.svg # 总文件数徽章
├── {language}-lines.json # 各语言代码行数徽章 ├── language-count.svg # 语言种类徽章
├── {language}-files.json # 各语言文件数徽章 └── {language}-lines.svg # 各语言代码行数徽章
└── README.md # 详细统计报告
``` ```
## 🔄 更新机制 ## 🔄 更新机制
@ -389,7 +372,7 @@ jobs:
--- ---
*🤖 由 GitHub Actions 自动维护* *🤖 由 Gitea Actions 自动维护*
EOF EOF
# 创建徽章目录 # 创建徽章目录
@ -433,20 +416,20 @@ jobs:
echo "" echo ""
# 构建排除参数 # 构建排除参数
EXCLUDE_PARAMS="" EXCLUDE_FIND_ARGS=()
IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}" IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}"
for dir in "${EXCLUDES[@]}"; do for dir in "${EXCLUDES[@]}"; do
EXCLUDE_PARAMS="$EXCLUDE_PARAMS -not -path '*/$dir/*'" EXCLUDE_FIND_ARGS+=(-not -path "*/$dir/*")
done done
# 获取所有文件 # 获取所有文件
echo "🔍 扫描文件..." echo "🔍 扫描文件..."
# 统计总文件数 # 统计总文件数
TOTAL_FILES=$(eval "find . -type f $EXCLUDE_PARAMS" | wc -l) TOTAL_FILES=$(find . -type f "${EXCLUDE_FIND_ARGS[@]}" | wc -l)
# 统计总代码行数(排除空行) # 统计总代码行数(排除空行)
TOTAL_CODE=$(eval "find . -type f $EXCLUDE_PARAMS -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null" | awk -F: '{sum+=$2} END {print sum+0}') TOTAL_CODE=$(find . -type f "${EXCLUDE_FIND_ARGS[@]}" -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
echo "📊 统计结果:" echo "📊 统计结果:"
echo " - 总文件数: $TOTAL_FILES" echo " - 总文件数: $TOTAL_FILES"
@ -489,10 +472,10 @@ jobs:
mkdir -p /tmp/lang_stats mkdir -p /tmp/lang_stats
# 构建排除参数 # 构建排除参数
EXCLUDE_PARAMS="" EXCLUDE_FIND_ARGS=()
IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}" IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}"
for dir in "${EXCLUDES[@]}"; do for dir in "${EXCLUDES[@]}"; do
EXCLUDE_PARAMS="$EXCLUDE_PARAMS -not -path '*/$dir/*'" EXCLUDE_FIND_ARGS+=(-not -path "*/$dir/*")
done done
LANGUAGE_COUNT=0 LANGUAGE_COUNT=0
@ -507,22 +490,22 @@ jobs:
echo "🔍 统计 $display_name..." echo "🔍 统计 $display_name..."
# 构建扩展名查找条件 # 构建扩展名查找条件
FIND_CONDITIONS="" LANG_FIND_ARGS=()
IFS=',' read -ra EXTS <<< "$extensions" IFS=',' read -ra EXTS <<< "$extensions"
for ext in "${EXTS[@]}"; do for ext in "${EXTS[@]}"; do
if [ -z "$FIND_CONDITIONS" ]; then if [ "${#LANG_FIND_ARGS[@]}" -eq 0 ]; then
FIND_CONDITIONS="-name '*.$ext'" LANG_FIND_ARGS+=(-name "*.$ext")
else else
FIND_CONDITIONS="$FIND_CONDITIONS -o -name '*.$ext'" LANG_FIND_ARGS+=(-o -name "*.$ext")
fi fi
done done
# 统计文件数 # 统计文件数
FILE_COUNT=$(eval "find . -type f \( $FIND_CONDITIONS \) $EXCLUDE_PARAMS" | wc -l) FILE_COUNT=$(find . -type f \( "${LANG_FIND_ARGS[@]}" \) "${EXCLUDE_FIND_ARGS[@]}" | wc -l)
if [ "$FILE_COUNT" -gt 0 ]; then if [ "$FILE_COUNT" -gt 0 ]; then
# 统计代码行数 # 统计代码行数
CODE_LINES=$(eval "find . -type f \( $FIND_CONDITIONS \) $EXCLUDE_PARAMS -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null" | awk -F: '{sum+=$2} END {print sum+0}') CODE_LINES=$(find . -type f \( "${LANG_FIND_ARGS[@]}" \) "${EXCLUDE_FIND_ARGS[@]}" -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
# 只保存超过阈值的语言 # 只保存超过阈值的语言
if [ "$CODE_LINES" -ge "${{ env.MIN_LINES_THRESHOLD }}" ]; then if [ "$CODE_LINES" -ge "${{ env.MIN_LINES_THRESHOLD }}" ]; then
@ -555,212 +538,50 @@ jobs:
echo "======================================" echo "======================================"
echo "" echo ""
- name: 🎨 生成徽章数据 - name: 🎨 生成 SVG 徽章与统计报告
id: generate_badges id: generate_badges
run: | run: |
echo "======================================" echo "======================================"
echo "🎨 生成徽章数据" echo "🎨 生成 SVG 徽章与统计报告"
echo "======================================" echo "======================================"
cd "${{ env.REPO_DIR }}" cd "${{ env.REPO_DIR }}"
# 确保在统计分支 # 在主分支渲染产物,避免依赖 stats 分支中的脚本文件
git checkout ${{ github.ref_name }}
RENDER_ROOT=$(mktemp -d)
UPDATED_AT=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
python3 .gitea/ci/render_stats_svgs.py \
--output-dir "$RENDER_ROOT/${{ env.BADGE_DIR }}" \
--readme-path "$RENDER_ROOT/README.md" \
--repo "${{ github.repository }}" \
--generated-by "Gitea Actions" \
--updated-at "$UPDATED_AT" \
--total-color "${{ env.COLOR_TOTAL }}" \
--files-color "${{ env.COLOR_FILES }}" \
--language-count-color "${{ env.COLOR_LANGUAGE_COUNT }}" \
--total-code "${{ steps.total.outputs.total_code }}" \
--formatted-total-code "${{ steps.total.outputs.formatted_code }}" \
--total-files "${{ steps.total.outputs.total_files }}" \
--formatted-total-files "${{ steps.total.outputs.formatted_files }}" \
--language-count "${{ steps.languages.outputs.language_count }}" \
--exclude-dirs "${{ env.EXCLUDE_DIRS }}" \
--min-lines-threshold "${{ env.MIN_LINES_THRESHOLD }}" \
--lang-summary /tmp/lang_summary.txt
git checkout ${{ env.BADGE_BRANCH }} git checkout ${{ env.BADGE_BRANCH }}
rm -rf "${{ env.BADGE_DIR }}"
# 确保徽章目录存在 mkdir -p "${{ env.BADGE_DIR }}"
mkdir -p ${{ env.BADGE_DIR }} cp -f "$RENDER_ROOT/README.md" README.md
cp -f "$RENDER_ROOT/${{ env.BADGE_DIR }}"/* "${{ env.BADGE_DIR }}/"
GENERATED_COUNT=0 GENERATED_COUNT=$(find "$RENDER_ROOT/${{ env.BADGE_DIR }}" -maxdepth 1 -type f | wc -l | tr -d ' ')
rm -rf "$RENDER_ROOT"
# 生成总代码行数徽章
echo "📊 生成总代码行数徽章..."
cat > ${{ env.BADGE_DIR }}/total-lines.json << EOF
{
"schemaVersion": 1,
"label": "代码",
"message": "${{ steps.total.outputs.formatted_code }} 行",
"color": "${{ env.COLOR_TOTAL }}",
"style": "${{ env.BADGE_STYLE }}"
}
EOF
GENERATED_COUNT=$((GENERATED_COUNT + 1))
# 生成总文件数徽章
echo "📁 生成总文件数徽章..."
cat > ${{ env.BADGE_DIR }}/total-files.json << EOF
{
"schemaVersion": 1,
"label": "文件",
"message": "${{ steps.total.outputs.formatted_files }} 个",
"color": "${{ env.COLOR_FILES }}",
"style": "${{ env.BADGE_STYLE }}"
}
EOF
GENERATED_COUNT=$((GENERATED_COUNT + 1))
# 生成各语言徽章
if [ -f /tmp/lang_summary.txt ] && [ -s /tmp/lang_summary.txt ]; then
echo ""
echo "🌐 生成语言徽章..."
while IFS='|' read -r lang_id display_name code_lines formatted_lines file_count color icon; do
[ -z "$lang_id" ] && continue
echo " - $display_name"
# 生成代码行数徽章(使用 heredoc
if [ -n "$icon" ]; then
# 带图标的徽章
cat > ${{ env.BADGE_DIR }}/${lang_id}-lines.json << EOFJSON
{
"schemaVersion": 1,
"label": "$display_name",
"message": "$formatted_lines 行",
"color": "$color",
"style": "${{ env.BADGE_STYLE }}",
"namedLogo": "$icon"
}
EOFJSON
else
# 不带图标的徽章
cat > ${{ env.BADGE_DIR }}/${lang_id}-lines.json << EOFJSON
{
"schemaVersion": 1,
"label": "$display_name",
"message": "$formatted_lines 行",
"color": "$color",
"style": "${{ env.BADGE_STYLE }}"
}
EOFJSON
fi
GENERATED_COUNT=$((GENERATED_COUNT + 1))
# 生成文件数徽章
cat > ${{ env.BADGE_DIR }}/${lang_id}-files.json << EOFJSON
{
"schemaVersion": 1,
"label": "$display_name 文件",
"message": "$file_count 个",
"color": "$color",
"style": "${{ env.BADGE_STYLE }}"
}
EOFJSON
GENERATED_COUNT=$((GENERATED_COUNT + 1))
done < /tmp/lang_summary.txt
fi
echo "" echo ""
echo "generated_count=$GENERATED_COUNT" >> $GITHUB_OUTPUT echo "generated_count=$GENERATED_COUNT" >> $GITHUB_OUTPUT
echo "✅ 已生成 $GENERATED_COUNT 个徽章" echo "✅ 已生成 $GENERATED_COUNT 个 SVG 徽章"
echo "======================================"
echo ""
- name: 📝 生成详细统计报告
if: env.GENERATE_DETAILED_REPORT == 'true'
run: |
echo "======================================"
echo "📝 生成详细统计报告"
echo "======================================"
cd "${{ env.REPO_DIR }}"
# 确保在统计分支
git checkout ${{ env.BADGE_BRANCH }}
# 生成 README
cat > ${{ env.BADGE_DIR }}/README.md << 'EOFMD'
# 📊 代码统计详细报告
> 🤖 由 GitHub Actions 自动生成
>
> 📅 更新时间: TIMESTAMP_PLACEHOLDER
## 📈 总体统计
| 统计项 | 数值 | 徽章 |
|--------|------|------|
| 💻 总代码行数 | **TOTAL_CODE_PLACEHOLDER** 行 | ![代码行数](https://img.shields.io/endpoint?url=RAW_URL_PLACEHOLDER/BRANCH_PLACEHOLDER/BADGE_DIR_PLACEHOLDER/total-lines.json) |
| 📁 总文件数 | **TOTAL_FILES_PLACEHOLDER** 个 | ![文件数](https://img.shields.io/endpoint?url=RAW_URL_PLACEHOLDER/BRANCH_PLACEHOLDER/BADGE_DIR_PLACEHOLDER/total-files.json) |
| 🌐 语言种类 | **LANG_COUNT_PLACEHOLDER** 种 | - |
## 🌈 语言分布
EOFMD
# 替换占位符
sed -i "s|TIMESTAMP_PLACEHOLDER|$(date -u '+%Y-%m-%d %H:%M:%S UTC')|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|TOTAL_CODE_PLACEHOLDER|${{ steps.total.outputs.formatted_code }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|TOTAL_FILES_PLACEHOLDER|${{ steps.total.outputs.formatted_files }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|LANG_COUNT_PLACEHOLDER|${{ steps.languages.outputs.language_count }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|REPO_PLACEHOLDER|${{ github.repository }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|BRANCH_PLACEHOLDER|${{ env.BADGE_BRANCH }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|BADGE_DIR_PLACEHOLDER|${{ env.BADGE_DIR }}|g" ${{ env.BADGE_DIR }}/README.md
sed -i "s|RAW_URL_PLACEHOLDER|${{ env.RAW_URL_BASE }}|g" ${{ env.BADGE_DIR }}/README.md
# 添加语言统计表格
if [ -f /tmp/lang_summary.txt ] && [ -s /tmp/lang_summary.txt ]; then
cat >> ${{ env.BADGE_DIR }}/README.md << 'EOFTABLE'
| 语言 | 代码行数 | 文件数 | 占比 | 徽章 |
|------|----------|--------|------|------|
EOFTABLE
TOTAL_CODE=${{ steps.total.outputs.total_code }}
while IFS='|' read -r lang_id display_name code_lines formatted_lines file_count color icon; do
[ -z "$lang_id" ] && continue
if [ "$TOTAL_CODE" -gt 0 ]; then
PERCENT=$(echo "scale=1; $code_lines * 100 / $TOTAL_CODE" | bc)
else
PERCENT="0.0"
fi
cat >> ${{ env.BADGE_DIR }}/README.md << EOFLANG
| **${display_name}** | ${formatted_lines} | ${file_count} | ${PERCENT}% | ![${display_name}](https://img.shields.io/endpoint?url=${{ env.RAW_URL_BASE }}/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}/${lang_id}-lines.json) |
EOFLANG
done < /tmp/lang_summary.txt
fi
# 添加配置说明
cat >> ${{ env.BADGE_DIR }}/README.md << EOFMD
---
## ⚙️ 配置说明
### Token 配置
- 当前使用: **${{ steps.validate_token.outputs.token_type }}**
- 需要在 Settings -> Secrets 中配置 `WORKFLOW`
### 排除规则
当前排除的目录:
```
${{ env.EXCLUDE_DIRS }}
```
### 阈值设置
- 最小代码行数阈值: **${{ env.MIN_LINES_THRESHOLD }}** 行
- 低于此阈值的语言将不会生成徽章
### 徽章样式
- 当前样式: **${{ env.BADGE_STYLE }}**
- 可选样式: flat, flat-square, plastic, for-the-badge, social
---
<div align="center">
*📊 由 [GitHub Actions](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions) 自动生成和更新*
*🤖 生成时间: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*
</div>
EOFMD
echo "✅ 详细统计报告已生成"
echo "======================================" echo "======================================"
echo "" echo ""
@ -781,7 +602,7 @@ jobs:
git config user.email "${{ env.GIT_USER_EMAIL }}" git config user.email "${{ env.GIT_USER_EMAIL }}"
# 添加所有更改 # 添加所有更改
git add ${{ env.BADGE_DIR }}/ git add README.md ${{ env.BADGE_DIR }}/
# 检查是否有变更 # 检查是否有变更
if git diff --staged --quiet; then if git diff --staged --quiet; then
@ -801,7 +622,7 @@ jobs:
- 徽章数: ${{ steps.generate_badges.outputs.generated_count }} 个 - 徽章数: ${{ steps.generate_badges.outputs.generated_count }} 个
🔗 触发提交: ${GITHUB_SHA:0:7} 🔗 触发提交: ${GITHUB_SHA:0:7}
🤖 由 GitHub Actions 自动生成" 🤖 由 Gitea Actions 自动生成"
git commit -m "$COMMIT_MSG" git commit -m "$COMMIT_MSG"
@ -842,7 +663,7 @@ jobs:
## 🔗 快速链接 ## 🔗 快速链接
- 📊 [查看详细统计报告](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}/README.md) - 📊 [查看详细统计报告](${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 }}) - 🎨 [浏览徽章文件](${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/${{ github.ref_name }}/.gitea/workflows/update_stats_badge.yaml)
@ -924,7 +745,7 @@ jobs:
echo " - 统计分支: ${{ env.BADGE_BRANCH }}" echo " - 统计分支: ${{ env.BADGE_BRANCH }}"
echo "" echo ""
echo "🔗 查看详细报告:" echo "🔗 查看详细报告:"
echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}/README.md" echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/README.md"
echo "" echo ""
echo "🎨 徽章目录:" echo "🎨 徽章目录:"
echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}" echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}"
@ -957,7 +778,7 @@ jobs:
if: always() if: always()
run: | run: |
echo "🧹 清理临时文件..." echo "🧹 清理临时文件..."
rm -rf /tmp/lang_stats /tmp/lang_summary.txt /tmp/total_stats*.json rm -rf /tmp/lang_stats /tmp/lang_summary.txt
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}" echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}"
rm -rf "${{ env.JOB_WORKSPACE }}" rm -rf "${{ env.JOB_WORKSPACE }}"

View File

@ -15,9 +15,7 @@
## 📂 文档导航 ## 📂 文档导航
### 🚀 Runner ### 🚀 [DEPLOYMENT.md](./DEPLOYMENT.md)
#### [DEPLOYMENT.md](./DEPLOYMENT.md)
**Gitea Runner Docker 部署完整教程** **Gitea Runner Docker 部署完整教程**

View File

@ -63,14 +63,14 @@ git push origin 1.0.0
#### 2. 📊 代码统计徽章工作流 (`update_stats_badge.yaml`) #### 2. 📊 代码统计徽章工作流 (`update_stats_badge.yaml`)
**功能**:自动统计代码行数并生成徽章数据 **功能**:自动统计代码行数并生成 SVG 徽章与统计报告
**特性** **特性**
- 📈 统计总代码行数和文件数 - 📈 统计总代码行数和文件数
- 🌐 按语言分组统计 - 🌐 按语言分组统计
- 🎨 自动生成徽章 JSON 数据 - 🎨 自动生成仓库内自托管的 SVG 徽章
- 📊 生成统计摘要JSON 格式) - 📊 `stats` 分支根目录生成统计报告 README
- 🚫 灵活的目录排除配置 - 🚫 灵活的目录排除配置
**触发方式** **触发方式**
@ -85,7 +85,19 @@ git push origin main
**配置文件**[update_stats_badge.yaml](.gitea/workflows/update_stats_badge.yaml) **配置文件**[update_stats_badge.yaml](.gitea/workflows/update_stats_badge.yaml)
**markdown引用格式**: `![C++](https://img.shields.io/endpoint?url=https://你的gitea/用户名/仓库/raw/branch/分支/badges/cpp-lines.json)` **markdown引用格式**: `![C++](https://你的gitea/用户名/仓库/raw/branch/stats/badges/cpp-lines.svg)`
**产物结构**
```txt
stats
├── README.md
└── badges/
├── total-lines.svg
├── total-files.svg
├── language-count.svg
└── <language>-lines.svg
```
💡 **详细配置**:查看 `update_stats_badge.yaml` 文件顶部的 `env` 区域,包含语言分组、颜色、排除目录等配置 💡 **详细配置**:查看 `update_stats_badge.yaml` 文件顶部的 `env` 区域,包含语言分组、颜色、排除目录等配置

View File

@ -90,29 +90,6 @@ echo ""
if [ $FIXED_COUNT -gt 0 ]; then if [ $FIXED_COUNT -gt 0 ]; then
echo -e "${GREEN}✓ 所有问题已自动修复!${NC}" echo -e "${GREEN}✓ 所有问题已自动修复!${NC}"
echo ""
echo "建议执行以下命令重启容器:"
echo " docker compose down"
echo " docker compose build --no-cache"
echo " docker compose up -d"
echo ""
# 询问是否立即重启
read -p "是否立即重启容器? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "正在重启容器..."
docker compose down
docker compose build --no-cache
docker compose up -d
sleep 3
echo ""
echo "容器状态:"
docker compose ps
echo ""
echo "查看日志:"
docker logs gitea-runner --tail=30
fi
else else
echo -e "${GREEN}✓ 所有文件格式正确,无需修复!${NC}" echo -e "${GREEN}✓ 所有文件格式正确,无需修复!${NC}"
fi fi

View File

@ -20,6 +20,24 @@ mkdir -p "$PERSISTENT_BIN"
mkdir -p /var/log/supervisor mkdir -p /var/log/supervisor
mkdir -p /var/run mkdir -p /var/run
create_buildx_builder() {
docker buildx create \
--name gitea-multiarch \
--driver docker-container \
--bootstrap \
--use \
--config /data/buildx/buildkitd.toml 2>/dev/null || \
docker buildx use gitea-multiarch 2>/dev/null
}
ensure_buildx_builder() {
docker buildx use gitea-multiarch 2>/dev/null || {
echo "⚠ Recreating Buildx builder..."
docker buildx rm gitea-multiarch 2>/dev/null || true
create_buildx_builder
}
}
# ============================================ # ============================================
# 初始化 Docker Buildx 支持(可选) # 初始化 Docker Buildx 支持(可选)
# ============================================ # ============================================
@ -80,13 +98,13 @@ if [ "$ENABLE_BUILDX" = "true" ]; then
# 验证多架构支持 # 验证多架构支持
echo "Verifying multi-arch support..." echo "Verifying multi-arch support..."
if docker run --rm arm64v8/alpine uname -m > /dev/null 2>&1; then if docker run --rm --platform linux/arm64 alpine uname -m > /dev/null 2>&1; then
echo " ✓ arm64 support verified" echo " ✓ arm64 support verified"
else else
echo " ⚠ arm64 verification failed" echo " ⚠ arm64 verification failed"
fi fi
if docker run --rm amd64/alpine uname -m > /dev/null 2>&1; then if docker run --rm --platform linux/amd64 alpine uname -m > /dev/null 2>&1; then
echo " ✓ amd64 support verified" echo " ✓ amd64 support verified"
else else
echo " ⚠ amd64 verification failed" echo " ⚠ amd64 verification failed"
@ -104,13 +122,7 @@ if [ "$ENABLE_BUILDX" = "true" ]; then
EOF EOF
# 创建 Buildx builder # 创建 Buildx builder
docker buildx create \ create_buildx_builder
--name gitea-multiarch \
--driver docker-container \
--bootstrap \
--use \
--config /data/buildx/buildkitd.toml 2>/dev/null || \
docker buildx use gitea-multiarch 2>/dev/null
# 验证 # 验证
echo "Verifying Buildx..." echo "Verifying Buildx..."
@ -125,15 +137,7 @@ EOF
echo "✓ Buildx already configured" echo "✓ Buildx already configured"
# 确保 builder 可用 # 确保 builder 可用
docker buildx use gitea-multiarch 2>/dev/null || { ensure_buildx_builder
echo "⚠ Recreating Buildx builder..."
docker buildx rm gitea-multiarch 2>/dev/null || true
docker buildx create \
--name gitea-multiarch \
--driver docker-container \
--bootstrap \
--use 2>/dev/null
}
fi fi
echo "" echo ""

View File

@ -133,12 +133,11 @@ echo "✓ Configuration file generated!"
# 创建缓存目录 # 创建缓存目录
mkdir -p cache mkdir -p cache
# 使用 Python 修改配置(最可靠的方法) # 使用 Python 修改配置
echo "" echo ""
echo "Configuring runner settings..." echo "Configuring runner settings..."
if command -v python3 &> /dev/null; then python3 << PYEOF
python3 << PYEOF
import yaml import yaml
import sys import sys
@ -184,32 +183,6 @@ except Exception as e:
sys.exit(1) sys.exit(1)
PYEOF PYEOF
PYTHON_EXIT=$?
if [ $PYTHON_EXIT -ne 0 ]; then
echo ""
echo "⚠ Python configuration failed, using basic sed..."
# 基本的 sed 修改(只修改简单的值,不动 labels
sed -i 's/capacity: 1/capacity: 4/g' config.yaml || true
sed -i 's/enabled: false/enabled: true/g' config.yaml || true
sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true
echo "✓ Basic configuration applied"
echo " Note: Please manually verify labels in config.yaml match .runner"
fi
else
echo "⚠ Python3 not found, applying basic configuration..."
# 基本的 sed 修改
sed -i 's/capacity: 1/capacity: 4/g' config.yaml || true
sed -i 's/enabled: false/enabled: true/g' config.yaml || true
sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true
echo "✓ Basic configuration applied"
echo " Note: Labels will use act_runner defaults"
fi
# 验证配置文件 # 验证配置文件
echo "" echo ""
echo "Validating configuration..." echo "Validating configuration..."

View File

@ -5,10 +5,8 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
UPGRADE_HELPER="${SCRIPT_DIR}/upgrade.sh" UPGRADE_HELPER="${SCRIPT_DIR}/upgrade.sh"
DEFAULT_RUNNER_VERSION_FALLBACK="${DEFAULT_RUNNER_VERSION_FALLBACK:-0.2.13}" DEFAULT_RUNNER_VERSION_FALLBACK="${DEFAULT_RUNNER_VERSION_FALLBACK:-0.2.13}"
if [ -f "${UPGRADE_HELPER}" ]; then # shellcheck source=/dev/null
# shellcheck source=/dev/null source "${UPGRADE_HELPER}"
source "${UPGRADE_HELPER}"
fi
echo "==========================================" echo "=========================================="
echo " Gitea Runner Installation Script " echo " Gitea Runner Installation Script "
@ -42,15 +40,13 @@ echo "Available versions: https://dl.gitea.com/act_runner/"
echo "" echo ""
DEFAULT_RUNNER_VERSION="${DEFAULT_RUNNER_VERSION_FALLBACK}" DEFAULT_RUNNER_VERSION="${DEFAULT_RUNNER_VERSION_FALLBACK}"
if declare -F resolve_latest_version_or_fallback >/dev/null 2>&1; then DEFAULT_RUNNER_VERSION=$(resolve_latest_version_or_fallback "${DEFAULT_RUNNER_VERSION_FALLBACK}")
DEFAULT_RUNNER_VERSION=$(resolve_latest_version_or_fallback "${DEFAULT_RUNNER_VERSION_FALLBACK}")
fi
echo "Default install version: ${DEFAULT_RUNNER_VERSION}" echo "Default install version: ${DEFAULT_RUNNER_VERSION}"
read -p "Enter version to install (default: ${DEFAULT_RUNNER_VERSION}): " RUNNER_VERSION read -p "Enter version to install (default: ${DEFAULT_RUNNER_VERSION}): " RUNNER_VERSION
RUNNER_VERSION=${RUNNER_VERSION:-$DEFAULT_RUNNER_VERSION} RUNNER_VERSION=${RUNNER_VERSION:-$DEFAULT_RUNNER_VERSION}
if declare -F validate_version >/dev/null 2>&1 && ! validate_version "${RUNNER_VERSION}"; then if ! validate_version "${RUNNER_VERSION}"; then
echo "✗ Invalid version format: ${RUNNER_VERSION}" echo "✗ Invalid version format: ${RUNNER_VERSION}"
exit 1 exit 1
fi fi
@ -118,6 +114,7 @@ echo ""
# 下载到持久化目录 # 下载到持久化目录
if curl -L "$DOWNLOAD_URL" -o "$INSTALL_PATH"; then if curl -L "$DOWNLOAD_URL" -o "$INSTALL_PATH"; then
chmod +x "$INSTALL_PATH" chmod +x "$INSTALL_PATH"
validate_binary_arch_or_fail "$INSTALL_PATH" "$RUNNER_ARCH"
# 同时创建软链接到系统路径 # 同时创建软链接到系统路径
ln -sf "$INSTALL_PATH" "$SYSTEM_PATH" ln -sf "$INSTALL_PATH" "$SYSTEM_PATH"

View File

@ -88,6 +88,73 @@ detect_runner_arch() {
esac esac
} }
detect_binary_arch_from_file() {
local binary_path=$1
local magic=""
local low_byte=""
local high_byte=""
local machine_hex=""
if [ ! -f "${binary_path}" ]; then
echo "unknown"
return 0
fi
magic=$(od -An -t x1 -N 4 "${binary_path}" 2>/dev/null | tr -d ' \n')
if [ "${magic}" != "7f454c46" ]; then
echo "unknown"
return 0
fi
read -r low_byte high_byte <<<"$(od -An -t x1 -j 18 -N 2 "${binary_path}" 2>/dev/null)"
if [ -z "${low_byte:-}" ] || [ -z "${high_byte:-}" ]; then
echo "unknown"
return 0
fi
machine_hex="${high_byte}${low_byte}"
case "${machine_hex}" in
003e)
echo "amd64"
;;
00b7)
echo "arm64"
;;
0028)
echo "arm-7"
;;
*)
echo "unknown"
;;
esac
}
binary_arch_matches_target() {
local binary_path=$1
local expected_arch=$2
local detected_arch=""
detected_arch=$(detect_binary_arch_from_file "${binary_path}")
[ "${detected_arch}" = "${expected_arch}" ]
}
validate_binary_arch_or_fail() {
local binary_path=$1
local expected_arch=$2
local detected_arch=""
detected_arch=$(detect_binary_arch_from_file "${binary_path}")
if [ "${detected_arch}" = "unknown" ]; then
fail "Unable to detect downloaded binary architecture: ${binary_path}"
fi
if [ "${detected_arch}" != "${expected_arch}" ]; then
fail "Downloaded binary architecture mismatch: expected ${expected_arch}, got ${detected_arch}"
fi
}
get_installed_version() { get_installed_version() {
local output="" local output=""
@ -102,6 +169,16 @@ get_installed_version() {
fi fi
} }
get_installed_binary_arch() {
if [ -x "${INSTALL_PATH}" ]; then
detect_binary_arch_from_file "${INSTALL_PATH}"
elif command -v act_runner >/dev/null 2>&1; then
detect_binary_arch_from_file "$(command -v act_runner)"
else
echo "unknown"
fi
}
list_registered_runners() { list_registered_runners() {
local runner_dir local runner_dir
@ -154,6 +231,7 @@ download_and_install() {
log "Downloading ${url}" log "Downloading ${url}"
curl -fL "${url}" -o "${tmp_path}" curl -fL "${url}" -o "${tmp_path}"
chmod +x "${tmp_path}" chmod +x "${tmp_path}"
validate_binary_arch_or_fail "${tmp_path}" "${arch}"
downloaded_version=$("${tmp_path}" --version 2>/dev/null | extract_version_number) downloaded_version=$("${tmp_path}" --version 2>/dev/null | extract_version_number)
if [ "${downloaded_version}" != "${version}" ]; then if [ "${downloaded_version}" != "${version}" ]; then
@ -205,6 +283,7 @@ main() {
local target_arch="" local target_arch=""
local auto_confirm="false" local auto_confirm="false"
local current_version="" local current_version=""
local current_arch="unknown"
local runner_names=() local runner_names=()
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
@ -252,13 +331,18 @@ main() {
target_arch=$(detect_runner_arch) target_arch=$(detect_runner_arch)
fi fi
if [ -n "${current_version}" ] && [ "${current_version}" = "${target_version}" ]; then current_arch=$(get_installed_binary_arch || true)
log "act_runner is already at the latest version: ${current_version}"
if [ -n "${current_version}" ] && \
[ "${current_version}" = "${target_version}" ] && \
[ "${current_arch}" = "${target_arch}" ]; then
log "act_runner is already at the requested version and architecture: ${current_version} (${current_arch})"
exit 0 exit 0
fi fi
log "Upgrade summary:" log "Upgrade summary:"
log " Current version: ${current_version:-not installed}" log " Current version: ${current_version:-not installed}"
log " Current arch: ${current_arch:-unknown}"
log " Target version: ${target_version}" log " Target version: ${target_version}"
log " Architecture: ${target_arch}" log " Architecture: ${target_arch}"
log " Install path: ${INSTALL_PATH}" log " Install path: ${INSTALL_PATH}"

View File

@ -1,2 +1,4 @@
GITEA_INSTANCE=https://git.mytsl.cn GITEA_INSTANCE=https://git.mytsl.cn
GITEA_TOKEN= GITEA_TOKEN=
DEFAULT_RUNNER_NAME=buildx-archlinux
DEFAULT_RUNNER_LABEL=archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest

View File

@ -23,8 +23,8 @@ services:
- BINFMT_METHOD=tonistiigi - BINFMT_METHOD=tonistiigi
- GITEA_INSTANCE=${GITEA_INSTANCE} - GITEA_INSTANCE=${GITEA_INSTANCE}
- GITEA_TOKEN=${GITEA_TOKEN} - GITEA_TOKEN=${GITEA_TOKEN}
- DEFAULT_RUNNER_NAME=buildx-archlinux - DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-buildx-archlinux}
- DEFAULT_RUNNER_LABEL=archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest - DEFAULT_RUNNER_LABEL=${DEFAULT_RUNNER_LABEL:-archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest}
# 如果需要使用代理,取消下面的注释并修改为你的代理地址 # 如果需要使用代理,取消下面的注释并修改为你的代理地址
# 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP # 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP

View File

@ -1,2 +1,4 @@
GITEA_INSTANCE=https://git.mytsl.cn GITEA_INSTANCE=https://git.mytsl.cn
GITEA_TOKEN= GITEA_TOKEN=
DEFAULT_RUNNER_NAME=buildx-ubuntu-22
DEFAULT_RUNNER_LABEL=ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,buildx-ubuntu-22:host://ubuntu:22.04

View File

@ -22,8 +22,8 @@ services:
- ENABLE_BUILDX=true - ENABLE_BUILDX=true
- GITEA_INSTANCE=${GITEA_INSTANCE} - GITEA_INSTANCE=${GITEA_INSTANCE}
- GITEA_TOKEN=${GITEA_TOKEN} - GITEA_TOKEN=${GITEA_TOKEN}
- DEFAULT_RUNNER_NAME=buildx-ubuntu-22 - DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-buildx-ubuntu-22}
- DEFAULT_RUNNER_LABEL=ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,buildx-ubuntu-22:host://ubuntu:22.04 - 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}
# 如果需要使用代理,取消下面的注释并修改为你的代理地址 # 如果需要使用代理,取消下面的注释并修改为你的代理地址
# 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP # 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP

View File

@ -1,2 +1,4 @@
GITEA_INSTANCE=https://git.mytsl.cn GITEA_INSTANCE=https://git.mytsl.cn
GITEA_TOKEN= GITEA_TOKEN=
DEFAULT_RUNNER_NAME=standard-ubuntu-22
DEFAULT_RUNNER_LABEL=ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,standard-ubuntu-22:host://ubuntu:22.04

View File

@ -21,8 +21,8 @@ services:
- ENABLE_BUILDX=false - ENABLE_BUILDX=false
- GITEA_INSTANCE=${GITEA_INSTANCE} - GITEA_INSTANCE=${GITEA_INSTANCE}
- GITEA_TOKEN=${GITEA_TOKEN} - GITEA_TOKEN=${GITEA_TOKEN}
- DEFAULT_RUNNER_NAME=standard-ubuntu-22 - DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-standard-ubuntu-22}
- DEFAULT_RUNNER_LABEL=ubuntu-22.04:host://ubuntu:22.04,company-server:host://ubuntu:22.04,standard-ubuntu-22:host://ubuntu:22.04 - 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}
# 如果需要使用代理,取消下面的注释并修改为你的代理地址 # 如果需要使用代理,取消下面的注释并修改为你的代理地址
# 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP # 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP

View File

@ -38,6 +38,10 @@ test_check_crlf_works_from_preset_directory() {
) )
! 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 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 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" ! has_crlf "${common_dir}/${file_name}" || fail "${file_name} should have CRLF fixed"

View File

@ -0,0 +1,57 @@
#!/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 '<svg' "${output_dir}/total-lines.svg" || fail "svg badges should contain svg markup"
grep -q '2,120' "${output_dir}/total-lines.svg" || fail "total-lines badge should include formatted total code"
grep -q 'Python' "${output_dir}/python-lines.svg" || fail "language badge should include display name"
grep -q '\./badges/total-lines.svg' "${readme_path}" || fail "README should reference badges via relative svg paths"
grep -q '\./badges/python-lines.svg' "${readme_path}" || fail "README should reference language badges via relative svg paths"
grep -q 'Gitea Actions' "${readme_path}" || fail "README should mention Gitea Actions"
grep -q '此分支由 Gitea Actions 自动生成和维护' "${readme_path}" || fail "README should explain that stats branch is generated"
grep -q '请勿手动修改此分支的内容' "${readme_path}" || fail "README should warn against manual edits"
! grep -q 'GitHub Actions' "${readme_path}" || fail "README should not mention GitHub Actions"
! find "${temp_root}" -name '*.json' | grep -q . || fail "renderer should not create json badge payloads"
rm -rf "${temp_root}"
echo "stats_svg_render_test.sh: PASS"

View File

@ -60,10 +60,12 @@ test_workflow_doc_describes_workspace_architecture() {
file="${REPO_ROOT}/WORKFLOW.md" file="${REPO_ROOT}/WORKFLOW.md"
grep -q 'Git bare 镜像 + 工作副本' "${file}" || fail "WORKFLOW.md should explicitly describe the bare mirror plus worktree-style model" grep -Eq '^## .*运行模型' "${file}" || fail "WORKFLOW.md should include a run model section"
grep -q '共享仓库缓存' "${file}" || fail "WORKFLOW.md should describe the shared repository cache model" grep -q 'bare' "${file}" || fail "WORKFLOW.md should describe the bare mirror model"
grep -q '独立工作副本' "${file}" || fail "WORKFLOW.md should describe isolated per-job work copies" grep -q '工作副本' "${file}" || fail "WORKFLOW.md should describe isolated work copies"
grep -q '任务结束后' "${file}" || fail "WORKFLOW.md should mention cleanup after workflow completion" 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"
! rg -q 'bootstrap_workspace\.sh' "${file}" || fail "WORKFLOW.md should describe architecture rather than helper implementation" ! rg -q 'bootstrap_workspace\.sh' "${file}" || fail "WORKFLOW.md should describe architecture rather than helper implementation"
! rg -q '/data/git-mirrors|/home/workspace/jobs' "${file}" || fail "WORKFLOW.md should avoid implementation-specific workspace paths" ! rg -q '/data/git-mirrors|/home/workspace/jobs' "${file}" || fail "WORKFLOW.md should avoid implementation-specific workspace paths"
} }
@ -88,13 +90,13 @@ test_readme_has_project_intro_and_navigation() {
file="${REPO_ROOT}/README.md" file="${REPO_ROOT}/README.md"
grep -q '^## 📖 项目简介$' "${file}" || fail "README.md should provide a project intro section" grep -Eq '^## .*项目简介' "${file}" || fail "README.md should provide a project intro section"
grep -q '^## 📂 文档导航$' "${file}" || fail "README.md should provide a document navigation 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 '\[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" grep -q '\[WORKFLOW.md\](\./WORKFLOW.md)' "${file}" || fail "README.md should link to WORKFLOW.md"
grep -q '具体部署步骤.*\[DEPLOYMENT.md\](\./DEPLOYMENT.md)' "${file}" || fail "README.md should delegate deployment details to DEPLOYMENT.md"
! rg -q '^## 🚀 快速提示$' "${file}" || fail "README.md should not carry concrete deployment tip sections" ! 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 '^## ⚙️ 当前默认行为$' "${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() { test_deployment_doc_stays_runner_focused() {
@ -124,9 +126,111 @@ test_preset_env_examples_exist() {
test -f "${file}" || fail "missing env example: ${file}" 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_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 '^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 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_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_preset_compose_uses_env_for_instance
test_workflows_do_not_hardcode_company_server test_workflows_do_not_hardcode_company_server
test_stats_workflow_uses_workflow_secret_consistently test_stats_workflow_uses_workflow_secret_consistently
@ -138,5 +242,12 @@ test_readme_has_project_intro_and_navigation
test_deployment_doc_stays_runner_focused test_deployment_doc_stays_runner_focused
test_presets_define_expected_hostname test_presets_define_expected_hostname
test_preset_env_examples_exist 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_entrypoint_uses_platform_aware_multiarch_verification
echo "template_defaults_test.sh: PASS" echo "template_defaults_test.sh: PASS"

View File

@ -20,6 +20,31 @@ assert_eq() {
fi 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() { test_extract_versions_from_listing() {
local html local html
html=$(cat <<'EOF' html=$(cat <<'EOF'
@ -90,10 +115,61 @@ test_resolve_latest_version_or_fallback_uses_fallback() {
"resolve_latest_version_or_fallback should fall back when detected version is invalid" "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_extract_versions_from_listing
test_pick_latest_version test_pick_latest_version
test_extract_version_number_accepts_v_prefix test_extract_version_number_accepts_v_prefix
test_resolve_latest_version_or_fallback_prefers_latest test_resolve_latest_version_or_fallback_prefers_latest
test_resolve_latest_version_or_fallback_uses_fallback test_resolve_latest_version_or_fallback_uses_fallback
test_detect_binary_arch_from_file
test_binary_arch_matches_target
echo "upgrade_test.sh: PASS" echo "upgrade_test.sh: PASS"

View File

@ -53,14 +53,6 @@ test_sanitize_job_name() {
assert_eq "release-notes-job" "${actual}" "job names should be filesystem-safe" assert_eq "release-notes-job" "${actual}" "job names should be filesystem-safe"
} }
test_build_lock_path() {
local actual
actual=$(build_mirror_lock_path "/data/git-mirrors" "csh" "actions-template")
assert_eq "/data/git-mirrors/csh/actions-template.git.lock" "${actual}" "lock path should sit beside the bare mirror"
}
test_repo_owner_and_name_parsing() { test_repo_owner_and_name_parsing() {
local actual local actual
@ -167,7 +159,6 @@ test_presets_do_not_mount_workspace_helper() {
test_repo_path_layout test_repo_path_layout
test_job_identity_prefers_run_metadata test_job_identity_prefers_run_metadata
test_sanitize_job_name test_sanitize_job_name
test_build_lock_path
test_repo_owner_and_name_parsing test_repo_owner_and_name_parsing
test_prepare_and_cleanup_workspace test_prepare_and_cleanup_workspace
test_register_default_capacity_is_four test_register_default_capacity_is_four