Skip to content

feat: 更新 f8x 使用指南,添加 CLI 参考和工具分类信息 #148

feat: 更新 f8x 使用指南,添加 CLI 参考和工具分类信息

feat: 更新 f8x 使用指南,添加 CLI 参考和工具分类信息 #148

name: Build and Deploy
on:
push:
branches: [ "master" ]
paths-ignore:
- 'public/**'
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild current version'
required: false
default: false
type: boolean
permissions:
contents: write
pages: write
id-token: write
checks: read
actions: read
jobs:
wait-for-validation:
runs-on: ubuntu-latest
steps:
- name: Brief sleep
run: sleep 15
- name: Wait for validation to complete
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ github.ref }}
check-name: 'Validate Standards'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- name: Wait for Secret Scan
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ github.ref }}
check-name: 'Scan for Secrets (AK/SK/Passwords)'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
build-and-deploy:
needs: [wait-for-validation]
runs-on: ubuntu-latest
steps:
# 1. 拉取源码 (最新版)
- name: Checkout Source
uses: actions/checkout@v4
with:
fetch-depth: 1
# 2. 下载旧的 index.json (用于合并数据,无需下载 ZIP)
- name: Fetch Current Index
env:
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
# 构造 URL
INDEX_URL="https://redc.${REPO_OWNER}.org/index.json"
echo "⬇️ Fetching index from: $INDEX_URL"
# 尝试下载,如果失败(比如第一次运行)则创建一个空的 JSON
curl -sL "$INDEX_URL" -o prev_index.json || echo '{"templates": {}}' > prev_index.json
# 检查文件内容是否合法,不合法则重置
if ! jq . prev_index.json >/dev/null 2>&1; then
echo '{"templates": {}}' > prev_index.json
fi
# 3. 构建脚本 (只打包变动的/新的)
- name: Build with Python
env:
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
FORCE_REBUILD: ${{ inputs.force_rebuild }}
run: |
mkdir -p public/templates
[ -f index.html ] && cp index.html public/index.html
[ -d assets ] && cp -r assets public/assets
# Inject latest redc version into index.html
REDC_VERSION=$(curl -sf https://api.github.com/repos/wgpsec/redc/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin).get('tag_name','v0.0.0'))" 2>/dev/null || echo "v0.0.0")
sed -i "s/__REDC_VERSION__/${REDC_VERSION}/g" public/index.html
python3 -c "
import os
import json
import shutil
import hashlib
import datetime
# --- 配置 ---
repo_owner = os.environ.get('REPO_OWNER')
repo_name = os.environ.get('REPO_NAME')
force_rebuild = os.environ.get('FORCE_REBUILD') == 'true'
base_url = f'https://redc.${REPO_OWNER}.org'
output_dir = 'public'
templates_dir = os.path.join(output_dir, 'templates')
# 旧索引路径
prev_index_path = 'prev_index.json'
if not os.path.exists(templates_dir): os.makedirs(templates_dir)
# --- A. 加载旧索引 ---
manifest_data = {}
try:
with open(prev_index_path, 'r', encoding='utf-8') as f:
old_json = json.load(f)
if isinstance(old_json.get('templates'), dict):
manifest_data = old_json['templates']
except:
print('⚠️ Failed to load prev_index.json, starting fresh.')
# --- B. 遍历处理 ---
repo_root = '.'
exclude = {'.git', '.github', 'public', 'previous_build', '.gitignore', 'index.html', 'plugins', 'skills', 'assets'}
# 标记本次构建是否有更新 (如果没有更新,甚至可以不提交 index.json,但为了更新时间戳还是提交吧)
has_changes = False
# 跟踪当前扫描到的所有模板 ID
current_template_ids = set()
for provider in sorted(os.listdir(repo_root)):
provider_path = os.path.join(repo_root, provider)
if not os.path.isdir(provider_path) or provider in exclude: continue
for template in sorted(os.listdir(provider_path)):
template_path = os.path.join(provider_path, template)
if not os.path.isdir(template_path): continue
template_id = f'{provider}/{template}'
current_template_ids.add(template_id)
# 1. 读取本地最新信息
meta_name = template; meta_user = 'Unknown'; meta_desc = 'No description.'; meta_version = '0.0.0'
meta_desc_en = ''; meta_tags = []
case_path = os.path.join(template_path, 'case.json')
try:
if os.path.exists(case_path):
with open(case_path, 'r', encoding='utf-8') as cf:
c = json.load(cf)
meta_name = c.get('name', template)
meta_user = c.get('user', 'Unknown')
meta_desc = c.get('description', 'No description.')
meta_version = c.get('version', '0.0.0')
meta_desc_en = c.get('description_en', '')
meta_tags = c.get('tags', [])
except: pass
# 读取 README
readme_content = ''
readme_path = os.path.join(template_path, 'README.md')
try:
if os.path.exists(readme_path):
with open(readme_path, 'r', encoding='utf-8') as rf: readme_content = rf.read()
except: pass
# 读取 README_EN
readme_en_content = ''
readme_en_path = os.path.join(template_path, 'README_EN.md')
try:
if os.path.exists(readme_en_path):
with open(readme_en_path, 'r', encoding='utf-8') as rf: readme_en_content = rf.read()
except: pass
# 2. 初始化元数据结构
if template_id not in manifest_data:
manifest_data[template_id] = {
'id': template_id, 'provider': provider, 'slug': template, 'versions': {}
}
# 总是更新通用元数据 (Title/Desc/Readme)
manifest_data[template_id]['latest'] = meta_version
manifest_data[template_id]['metadata'] = {
'name': meta_name, 'author': meta_user, 'description': meta_desc,
'description_en': meta_desc_en, 'tags': meta_tags, 'readme': readme_content,
'readme_en': readme_en_content
}
# 3. 检查版本是否存在
zip_filename = f'{provider}_{template}_v{meta_version}.zip'
zip_output_path = os.path.join(templates_dir, zip_filename)
version_exists = meta_version in manifest_data[template_id]['versions']
# 只有当:强制重建 OR 版本不存在 时,才打包
if force_rebuild or not version_exists:
print(f'🔨 Packaging: {zip_filename}')
shutil.make_archive(zip_output_path.replace('.zip', ''), 'zip', root_dir=template_path)
h = hashlib.sha256()
with open(zip_output_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''): h.update(chunk)
manifest_data[template_id]['versions'][meta_version] = {
'url': f'{base_url}/templates/{zip_filename}',
'sha256': h.hexdigest(),
'updated_at': datetime.datetime.utcnow().isoformat() + 'Z'
}
has_changes = True
else:
print(f'⏩ Skipping: {zip_filename} (Version exists in index)')
# --- 删除不再存在的模板 ---
deleted_templates = [tid for tid in manifest_data.keys() if tid not in current_template_ids]
for template_id in deleted_templates:
print(f'🗑️ Removing deleted template: {template_id}')
del manifest_data[template_id]
has_changes = True
# --- C. 保存 Index ---
# 即使没有新 ZIP,更新一下 updated_at 也是好的
final_manifest = {
'updated_at': datetime.datetime.utcnow().isoformat() + 'Z',
'repo_name': repo_name,
'templates': manifest_data
}
with open(os.path.join(output_dir, 'index.json'), 'w', encoding='utf-8') as f:
json.dump(final_manifest, f, indent=2, ensure_ascii=False)
print(f'✨ Index updated.')
"
# 3.5 构建插件索引和 ZIP 包
- name: Build Plugin Registry
env:
REPO_OWNER: ${{ github.repository_owner }}
run: |
mkdir -p public/plugins
python3 -c "
import os
import json
import shutil
import hashlib
import datetime
repo_owner = os.environ.get('REPO_OWNER')
base_url = f'https://redc.{repo_owner}.org'
plugins_src = 'plugins'
plugins_out = os.path.join('public', 'plugins')
if not os.path.isdir(plugins_src):
print('⏩ No plugins/ directory, skipping')
exit(0)
os.makedirs(plugins_out, exist_ok=True)
# 加载已有的 registry (如果有)
prev_registry_url = f'{base_url}/plugins/plugin-registry.json'
prev_registry = {'version': 1, 'plugins': []}
prev_path = 'prev_plugin_registry.json'
os.system(f'curl -sL \"{prev_registry_url}\" -o {prev_path} 2>/dev/null || true')
try:
with open(prev_path, 'r') as f:
prev_registry = json.load(f)
except:
pass
# 已有插件的 hash 缓存
prev_hashes = {}
for p in prev_registry.get('plugins', []):
if 'sha256' in p:
prev_hashes[p['name']] = p['sha256']
plugins = []
for entry in sorted(os.listdir(plugins_src)):
plugin_dir = os.path.join(plugins_src, entry)
manifest_path = os.path.join(plugin_dir, 'plugin.json')
if not os.path.isdir(plugin_dir) or not os.path.exists(manifest_path):
continue
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except:
print(f'⚠️ Invalid plugin.json in {entry}, skipping')
continue
name = manifest.get('name', entry)
version = manifest.get('version', '0.0.0')
zip_filename = f'{name}_v{version}.zip'
zip_path = os.path.join(plugins_out, zip_filename)
# 计算源码 hash
h = hashlib.sha256()
for root, dirs, files in sorted(os.walk(plugin_dir)):
dirs.sort()
for fn in sorted(files):
fp = os.path.join(root, fn)
rel = os.path.relpath(fp, plugin_dir)
h.update(rel.encode())
with open(fp, 'rb') as ff:
for chunk in iter(lambda: ff.read(4096), b''):
h.update(chunk)
src_hash = h.hexdigest()
# 只有内容变化才重新打包
if src_hash != prev_hashes.get(name, ''):
print(f'🔨 Packaging plugin: {zip_filename}')
shutil.make_archive(zip_path.replace('.zip', ''), 'zip', root_dir=plugin_dir)
else:
print(f'⏩ Skipping plugin: {zip_filename} (unchanged)')
plugins.append({
'name': name,
'version': version,
'description': manifest.get('description', ''),
'description_en': manifest.get('description_en', ''),
'author': manifest.get('author', ''),
'category': manifest.get('category', ''),
'tags': manifest.get('tags', []),
'min_redc_version': manifest.get('min_redc_version', ''),
'url': f'{base_url}/plugins/{zip_filename}',
'sha256': src_hash
})
# 写入 plugin-registry.json
registry = {
'version': 1,
'updated': datetime.datetime.utcnow().strftime('%Y-%m-%d'),
'plugins': plugins
}
registry_path = os.path.join(plugins_out, 'plugin-registry.json')
with open(registry_path, 'w', encoding='utf-8') as f:
json.dump(registry, f, indent=2, ensure_ascii=False)
print(f'✨ Plugin registry updated: {len(plugins)} plugins')
"
# 3.6 构建 Skills 索引和 ZIP 包
- name: Build Skills Registry
env:
REPO_OWNER: ${{ github.repository_owner }}
run: |
mkdir -p public/skills
python3 -c "
import os
import json
import re
import shutil
import hashlib
import datetime
repo_owner = os.environ.get('REPO_OWNER')
base_url = f'https://redc.{repo_owner}.org'
skills_src = 'skills'
skills_out = os.path.join('public', 'skills')
if not os.path.isdir(skills_src):
print('⏩ No skills/ directory, skipping')
exit(0)
os.makedirs(skills_out, exist_ok=True)
# 加载已有的 registry
prev_registry_url = f'{base_url}/skills/skill-registry.json'
prev_hashes = {}
prev_path = 'prev_skill_registry.json'
os.system(f'curl -sL \"{prev_registry_url}\" -o {prev_path} 2>/dev/null || true')
try:
with open(prev_path, 'r') as f:
prev_data = json.load(f)
for s in prev_data.get('skills', []):
if 'sha256' in s:
prev_hashes[s['id']] = s['sha256']
except:
pass
skills = []
for entry in sorted(os.listdir(skills_src)):
skill_dir = os.path.join(skills_src, entry)
skill_md = os.path.join(skill_dir, 'SKILL.md')
if not os.path.isdir(skill_dir) or not os.path.exists(skill_md):
continue
# Parse YAML frontmatter
with open(skill_md, 'r', encoding='utf-8') as f:
content = f.read()
name = entry
description = ''
tags = []
if content.startswith('---'):
end = content.find('---', 3)
if end > 0:
front = content[3:end]
for line in front.strip().split('\n'):
line = line.strip()
if line.startswith('name:'):
name = line[5:].strip().strip('\"\\\"')
elif line.startswith('description:'):
description = line[12:].strip().strip('\"\\\"')
elif line.startswith('tags:'):
tag_str = line[5:].strip()
tags = [t.strip() for t in re.split(r'[,,]', tag_str) if t.strip()]
# Compute hash
h = hashlib.sha256()
for root, dirs, files in sorted(os.walk(skill_dir)):
dirs.sort()
for fn in sorted(files):
fp = os.path.join(root, fn)
rel = os.path.relpath(fp, skill_dir)
h.update(rel.encode())
with open(fp, 'rb') as ff:
for chunk in iter(lambda: ff.read(4096), b''):
h.update(chunk)
src_hash = h.hexdigest()
zip_filename = f'skill-{entry}.zip'
zip_path = os.path.join(skills_out, zip_filename)
if src_hash != prev_hashes.get(entry, ''):
print(f'🔨 Packaging skill: {zip_filename}')
shutil.make_archive(zip_path.replace('.zip', ''), 'zip', root_dir=skill_dir)
else:
print(f'⏩ Skipping skill: {zip_filename} (unchanged)')
skills.append({
'id': entry,
'name': name,
'description': description,
'tags': tags,
'url': f'{base_url}/skills/{zip_filename}',
'sha256': src_hash
})
registry = {
'version': 1,
'updated': datetime.datetime.utcnow().strftime('%Y-%m-%d'),
'skills': skills
}
registry_path = os.path.join(skills_out, 'skill-registry.json')
with open(registry_path, 'w', encoding='utf-8') as f:
json.dump(registry, f, indent=2, ensure_ascii=False)
print(f'✨ Skill registry updated: {len(skills)} skills')
"
# 4. 部署 (Keep Files 模式)
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
publish_branch: gh-pages
keep_files: true
commit_message: "Deploy: ${{ github.event.head_commit.message }}"