🐛 fix(stats): switch badges to self-hosted svg
Ubuntu System Information / show-system-info (push) Successful in 0s
Details
Ubuntu System Information / show-system-info (push) Successful in 0s
Details
This commit is contained in:
parent
d4fa498a4a
commit
1259495cbb
|
|
@ -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("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace("'", "'")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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" |"
|
||||||
|
)
|
||||||
|
|
||||||
|
if language_rows:
|
||||||
|
language_table = "\n".join(language_rows)
|
||||||
|
else:
|
||||||
|
language_table = "| _未找到符合条件的语言_ | - | - | - | - |"
|
||||||
|
|
||||||
|
return f"""# 📊 代码统计详细报告
|
||||||
|
|
||||||
|
> 此分支由 {args.generated_by} 自动生成和维护
|
||||||
|
>
|
||||||
|
> ⚠️ **请勿手动修改此分支的内容!**
|
||||||
|
>
|
||||||
|
> 🤖 由 {args.generated_by} 自动生成
|
||||||
|
>
|
||||||
|
> 📅 更新时间: {args.updated_at}
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 📈 总体统计
|
||||||
|
|
||||||
|
| 统计项 | 数值 | 徽章 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 💻 总代码行数 | **{args.formatted_total_code}** 行 |  |
|
||||||
|
| 📁 总文件数 | **{args.formatted_total_files}** 个 |  |
|
||||||
|
| 🌐 语言种类 | **{args.language_count}** 种 |  |
|
||||||
|
|
||||||
|
## 🌈 语言分布
|
||||||
|
|
||||||
|
| 语言 | 代码行数 | 文件数 | 占比 | 徽章 |
|
||||||
|
|------|----------|--------|------|------|
|
||||||
|
{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()
|
||||||
|
|
@ -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 服务器 URL(Gitea 示例: 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
|
||||||
|
|
||||||
# 创建徽章目录
|
# 创建徽章目录
|
||||||
|
|
@ -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** 行 |  |
|
|
||||||
| 📁 总文件数 | **TOTAL_FILES_PLACEHOLDER** 个 |  |
|
|
||||||
| 🌐 语言种类 | **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}% |  |
|
|
||||||
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 }}"
|
||||||
|
|
|
||||||
20
WORKFLOW.md
20
WORKFLOW.md
|
|
@ -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引用格式**: ``
|
**markdown引用格式**: ``
|
||||||
|
|
||||||
|
**产物结构**:
|
||||||
|
|
||||||
|
```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` 区域,包含语言分组、颜色、排除目录等配置
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -199,6 +199,27 @@ test_stats_workflow_avoids_eval_find() {
|
||||||
grep -q 'LANG_FIND_ARGS=()' "${file}" || fail "stats workflow should build language-specific find arguments via 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() {
|
test_entrypoint_uses_platform_aware_multiarch_verification() {
|
||||||
local file
|
local file
|
||||||
|
|
||||||
|
|
@ -226,6 +247,7 @@ test_register_requires_python_yaml_path
|
||||||
test_setup_requires_upgrade_helper
|
test_setup_requires_upgrade_helper
|
||||||
test_entrypoint_uses_shared_buildx_builder_creation
|
test_entrypoint_uses_shared_buildx_builder_creation
|
||||||
test_stats_workflow_avoids_eval_find
|
test_stats_workflow_avoids_eval_find
|
||||||
|
test_stats_workflow_uses_svg_badges
|
||||||
test_entrypoint_uses_platform_aware_multiarch_verification
|
test_entrypoint_uses_platform_aware_multiarch_verification
|
||||||
|
|
||||||
echo "template_defaults_test.sh: PASS"
|
echo "template_defaults_test.sh: PASS"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue