Compare commits

..

No commits in common. "1259495cbb0ebccd0972df1c0817b53e8e6e7736" and "0579c2dc9754b67148cf4d2fe057a163f6e4483a" have entirely different histories.

21 changed files with 355 additions and 701 deletions

View File

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

View File

@ -1,243 +0,0 @@
#!/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,12 +35,17 @@ env:
SPECIAL_INCLUDES: ""
# 示例: 'dist:js,css|vendor:go,mod'
# ===== SVG 徽章颜色配置 =====
# ===== 徽章颜色配置 =====
COLOR_TOTAL: "blue"
COLOR_FILES: "green"
COLOR_LANGUAGE_COUNT: "purple"
COLOR_DEFAULT: "brightgreen"
# ===== 徽章样式配置 =====
BADGE_STYLE: "flat" # 可选: flat, flat-square, plastic, for-the-badge, social
# ===== 输出配置 =====
# 是否生成详细报告
GENERATE_DETAILED_REPORT: "true"
# 是否输出到 workflow summary
OUTPUT_TO_SUMMARY: "true"
# 最小代码行数阈值(低于此值的语言不生成徽章)
@ -50,6 +55,18 @@ env:
GIT_USER_NAME: "ci[bot]"
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'
# ==========================================
# 🎨 语言分组配置
# 格式: 组名:后缀列表:显示名称:颜色:图标(可选)
@ -346,19 +363,19 @@ jobs:
cat > README.md << 'EOF'
# 📊 代码统计徽章数据
> 此分支由 Gitea Actions 自动生成和维护
> 此分支由 GitHub Actions 自动生成和维护
>
> ⚠️ **请勿手动修改此分支的内容!**
## 📁 目录结构
```
README.md # 详细统计报告
badges/
├── total-lines.svg # 总代码行数徽章
├── total-files.svg # 总文件数徽章
├── language-count.svg # 语言种类徽章
└── {language}-lines.svg # 各语言代码行数徽章
├── total-lines.json # 总代码行数徽章
├── total-files.json # 总文件数徽章
├── {language}-lines.json # 各语言代码行数徽章
├── {language}-files.json # 各语言文件数徽章
└── README.md # 详细统计报告
```
## 🔄 更新机制
@ -372,7 +389,7 @@ jobs:
---
*🤖 由 Gitea Actions 自动维护*
*🤖 由 GitHub Actions 自动维护*
EOF
# 创建徽章目录
@ -416,20 +433,20 @@ jobs:
echo ""
# 构建排除参数
EXCLUDE_FIND_ARGS=()
EXCLUDE_PARAMS=""
IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}"
for dir in "${EXCLUDES[@]}"; do
EXCLUDE_FIND_ARGS+=(-not -path "*/$dir/*")
EXCLUDE_PARAMS="$EXCLUDE_PARAMS -not -path '*/$dir/*'"
done
# 获取所有文件
echo "🔍 扫描文件..."
# 统计总文件数
TOTAL_FILES=$(find . -type f "${EXCLUDE_FIND_ARGS[@]}" | wc -l)
TOTAL_FILES=$(eval "find . -type f $EXCLUDE_PARAMS" | wc -l)
# 统计总代码行数(排除空行)
TOTAL_CODE=$(find . -type f "${EXCLUDE_FIND_ARGS[@]}" -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
TOTAL_CODE=$(eval "find . -type f $EXCLUDE_PARAMS -exec grep -cHv '^[[:space:]]*$' {} + 2>/dev/null" | awk -F: '{sum+=$2} END {print sum+0}')
echo "📊 统计结果:"
echo " - 总文件数: $TOTAL_FILES"
@ -472,10 +489,10 @@ jobs:
mkdir -p /tmp/lang_stats
# 构建排除参数
EXCLUDE_FIND_ARGS=()
EXCLUDE_PARAMS=""
IFS=',' read -ra EXCLUDES <<< "${{ env.EXCLUDE_DIRS }}"
for dir in "${EXCLUDES[@]}"; do
EXCLUDE_FIND_ARGS+=(-not -path "*/$dir/*")
EXCLUDE_PARAMS="$EXCLUDE_PARAMS -not -path '*/$dir/*'"
done
LANGUAGE_COUNT=0
@ -490,22 +507,22 @@ jobs:
echo "🔍 统计 $display_name..."
# 构建扩展名查找条件
LANG_FIND_ARGS=()
FIND_CONDITIONS=""
IFS=',' read -ra EXTS <<< "$extensions"
for ext in "${EXTS[@]}"; do
if [ "${#LANG_FIND_ARGS[@]}" -eq 0 ]; then
LANG_FIND_ARGS+=(-name "*.$ext")
if [ -z "$FIND_CONDITIONS" ]; then
FIND_CONDITIONS="-name '*.$ext'"
else
LANG_FIND_ARGS+=(-o -name "*.$ext")
FIND_CONDITIONS="$FIND_CONDITIONS -o -name '*.$ext'"
fi
done
# 统计文件数
FILE_COUNT=$(find . -type f \( "${LANG_FIND_ARGS[@]}" \) "${EXCLUDE_FIND_ARGS[@]}" | wc -l)
FILE_COUNT=$(eval "find . -type f \( $FIND_CONDITIONS \) $EXCLUDE_PARAMS" | wc -l)
if [ "$FILE_COUNT" -gt 0 ]; then
# 统计代码行数
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}')
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}')
# 只保存超过阈值的语言
if [ "$CODE_LINES" -ge "${{ env.MIN_LINES_THRESHOLD }}" ]; then
@ -538,50 +555,212 @@ jobs:
echo "======================================"
echo ""
- name: 🎨 生成 SVG 徽章与统计报告
- name: 🎨 生成徽章数据
id: generate_badges
run: |
echo "======================================"
echo "🎨 生成 SVG 徽章与统计报告"
echo "🎨 生成徽章数据"
echo "======================================"
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 }}
rm -rf "${{ 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=$(find "$RENDER_ROOT/${{ env.BADGE_DIR }}" -maxdepth 1 -type f | wc -l | tr -d ' ')
rm -rf "$RENDER_ROOT"
# 确保徽章目录存在
mkdir -p ${{ env.BADGE_DIR }}
GENERATED_COUNT=0
# 生成总代码行数徽章
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 "generated_count=$GENERATED_COUNT" >> $GITHUB_OUTPUT
echo "✅ 已生成 $GENERATED_COUNT 个 SVG 徽章"
echo "✅ 已生成 $GENERATED_COUNT 个徽章"
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 ""
@ -602,7 +781,7 @@ jobs:
git config user.email "${{ env.GIT_USER_EMAIL }}"
# 添加所有更改
git add README.md ${{ env.BADGE_DIR }}/
git add ${{ env.BADGE_DIR }}/
# 检查是否有变更
if git diff --staged --quiet; then
@ -622,7 +801,7 @@ jobs:
- 徽章数: ${{ steps.generate_badges.outputs.generated_count }} 个
🔗 触发提交: ${GITHUB_SHA:0:7}
🤖 由 Gitea Actions 自动生成"
🤖 由 GitHub Actions 自动生成"
git commit -m "$COMMIT_MSG"
@ -663,7 +842,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 }}/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)
@ -745,7 +924,7 @@ jobs:
echo " - 统计分支: ${{ env.BADGE_BRANCH }}"
echo ""
echo "🔗 查看详细报告:"
echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/README.md"
echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}/README.md"
echo ""
echo "🎨 徽章目录:"
echo " ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.BADGE_BRANCH }}/${{ env.BADGE_DIR }}"
@ -778,7 +957,7 @@ jobs:
if: always()
run: |
echo "🧹 清理临时文件..."
rm -rf /tmp/lang_stats /tmp/lang_summary.txt
rm -rf /tmp/lang_stats /tmp/lang_summary.txt /tmp/total_stats*.json
if [ -n "${{ env.JOB_WORKSPACE }}" ] && [ "${{ env.JOB_WORKSPACE }}" != "/" ]; then
echo "🧹 清理 Job 工作区: ${{ env.JOB_WORKSPACE }}"
rm -rf "${{ env.JOB_WORKSPACE }}"

View File

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

View File

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

View File

@ -90,6 +90,29 @@ echo ""
if [ $FIXED_COUNT -gt 0 ]; then
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
echo -e "${GREEN}✓ 所有文件格式正确,无需修复!${NC}"
fi

View File

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

View File

@ -133,10 +133,11 @@ echo "✓ Configuration file generated!"
# 创建缓存目录
mkdir -p cache
# 使用 Python 修改配置
# 使用 Python 修改配置(最可靠的方法)
echo ""
echo "Configuring runner settings..."
if command -v python3 &> /dev/null; then
python3 << PYEOF
import yaml
import sys
@ -183,6 +184,32 @@ except Exception as e:
sys.exit(1)
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 "Validating configuration..."

View File

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

View File

@ -88,73 +88,6 @@ detect_runner_arch() {
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() {
local output=""
@ -169,16 +102,6 @@ get_installed_version() {
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() {
local runner_dir
@ -231,7 +154,6 @@ download_and_install() {
log "Downloading ${url}"
curl -fL "${url}" -o "${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)
if [ "${downloaded_version}" != "${version}" ]; then
@ -283,7 +205,6 @@ main() {
local target_arch=""
local auto_confirm="false"
local current_version=""
local current_arch="unknown"
local runner_names=()
while [ $# -gt 0 ]; do
@ -331,18 +252,13 @@ main() {
target_arch=$(detect_runner_arch)
fi
current_arch=$(get_installed_binary_arch || true)
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})"
if [ -n "${current_version}" ] && [ "${current_version}" = "${target_version}" ]; then
log "act_runner is already at the latest version: ${current_version}"
exit 0
fi
log "Upgrade summary:"
log " Current version: ${current_version:-not installed}"
log " Current arch: ${current_arch:-unknown}"
log " Target version: ${target_version}"
log " Architecture: ${target_arch}"
log " Install path: ${INSTALL_PATH}"

View File

@ -1,4 +1,2 @@
GITEA_INSTANCE=https://git.mytsl.cn
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
- GITEA_INSTANCE=${GITEA_INSTANCE}
- GITEA_TOKEN=${GITEA_TOKEN}
- DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-buildx-archlinux}
- DEFAULT_RUNNER_LABEL=${DEFAULT_RUNNER_LABEL:-archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest}
- DEFAULT_RUNNER_NAME=buildx-archlinux
- DEFAULT_RUNNER_LABEL=archlinux:host://archlinux:latest,company-server:host://archlinux:latest,buildx-archlinux:host://archlinux:latest
# 如果需要使用代理,取消下面的注释并修改为你的代理地址
# 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP

View File

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

View File

@ -1,4 +1,2 @@
GITEA_INSTANCE=https://git.mytsl.cn
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
- GITEA_INSTANCE=${GITEA_INSTANCE}
- GITEA_TOKEN=${GITEA_TOKEN}
- DEFAULT_RUNNER_NAME=${DEFAULT_RUNNER_NAME:-standard-ubuntu-22}
- 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}
- 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
# 如果需要使用代理,取消下面的注释并修改为你的代理地址
# 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP

View File

@ -38,10 +38,6 @@ 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 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"

View File

@ -1,57 +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 '<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,12 +60,10 @@ test_workflow_doc_describes_workspace_architecture() {
file="${REPO_ROOT}/WORKFLOW.md"
grep -Eq '^## .*运行模型' "${file}" || fail "WORKFLOW.md should include a run model section"
grep -q 'bare' "${file}" || fail "WORKFLOW.md should describe the bare mirror model"
grep -q '工作副本' "${file}" || fail "WORKFLOW.md should describe isolated work copies"
grep -Eq '共享.*缓存|缓存.*共享' "${file}" || fail "WORKFLOW.md should describe the shared cache model"
grep -Eq '并发|隔离' "${file}" || fail "WORKFLOW.md should mention concurrency or isolation tradeoffs"
grep -Eq '结束后.*清理|清理.*工作副本' "${file}" || fail "WORKFLOW.md should mention cleanup after workflow completion"
grep -q 'Git bare 镜像 + 工作副本' "${file}" || fail "WORKFLOW.md should explicitly describe the bare mirror plus worktree-style model"
grep -q '共享仓库缓存' "${file}" || fail "WORKFLOW.md should describe the shared repository cache model"
grep -q '独立工作副本' "${file}" || fail "WORKFLOW.md should describe isolated per-job work copies"
grep -q '任务结束后' "${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 '/data/git-mirrors|/home/workspace/jobs' "${file}" || fail "WORKFLOW.md should avoid implementation-specific workspace paths"
}
@ -90,13 +88,13 @@ test_readme_has_project_intro_and_navigation() {
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 '^## 📖 项目简介$' "${file}" || fail "README.md should provide a project intro section"
grep -q '^## 📂 文档导航$' "${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"
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 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() {
@ -126,111 +124,9 @@ test_preset_env_examples_exist() {
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_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
@ -242,12 +138,5 @@ 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_entrypoint_uses_platform_aware_multiarch_verification
echo "template_defaults_test.sh: PASS"

View File

@ -20,31 +20,6 @@ assert_eq() {
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'
@ -115,61 +90,10 @@ test_resolve_latest_version_or_fallback_uses_fallback() {
"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"

View File

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