name: ♻️ 更新第三方 Skills on: workflow_run: workflows: ["🧰 准备环境"] types: - completed concurrency: group: update-thirdparty-${{ github.repository }} cancel-in-progress: false env: # Prepare 维护共享仓库并读取代码;此处 token 用于 worktree 内的 fetch/push。 ACCESS_TOKEN: ${{ secrets.WORKFLOW }} WORKSPACE_ROOT: "/data/workspace" WORKSPACE_SLOT: "thirdparty" SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }} SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} THIRDPARTY_BRANCH: "thirdparty/skill" MANIFEST_PATH: ".gitea/ci/thirdparty_skills.json" GIT_USER_NAME: "ci[bot]" GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn" jobs: update_and_sync: # 仅在 Prepare 于主分支成功后触发(含每日 schedule 触发的 Prepare)。 if: ${{ github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') }} name: 📥 更新快照并同步 main runs-on: standard-ubuntu-22 permissions: contents: write steps: - name: 🔐 验证 Token 配置 shell: bash run: | set -euo pipefail if [ -z "$ACCESS_TOKEN" ]; then echo "❌ 未配置 WORKFLOW secret,无法 fetch/push" >&2 exit 1 fi echo "✅ Token 已配置" echo "🌿 上游分支: ${{ env.SOURCE_BRANCH }}" echo "📝 上游提交: ${{ env.SOURCE_SHA }}" - name: 📥 对齐 Worktree id: prepare_worktree shell: bash run: | set -euo pipefail REPO_NAME="${{ github.event.repository.name }}" HEAD_SHA="${{ env.SOURCE_SHA }}" REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git" WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/${WORKSPACE_SLOT}" WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock" case "$HEAD_SHA" in ''|*[!0-9a-fA-F]*) echo "无效的上游提交 SHA: $HEAD_SHA" >&2 exit 1 ;; esac if [ ! -d "$REPOSITORY_DIR" ] || \ [ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2 exit 1 fi if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2 exit 1 fi # 在共享仓库上配置读取 ACCESS_TOKEN 的凭证助手,供 worktree 内后续 # fetch/push 透明鉴权。token 不落盘,助手在 git 调用时从环境读取。 # 幂等:同一 runner 复用共享仓库时,credential.helper 已是多值(空值 + # helper),单值写入会报 "cannot overwrite multiple values";故先 unset # 再用两个 --add 重建(空值清空继承链,再追加读环境的助手)。 git --git-dir="$REPOSITORY_DIR" config credential.interactive never git --git-dir="$REPOSITORY_DIR" config --unset-all credential.helper 2>/dev/null || true git --git-dir="$REPOSITORY_DIR" config --add credential.helper '' git --git-dir="$REPOSITORY_DIR" config --add credential.helper '!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' # 固化 origin 到无 token 的 HTTPS URL:区域脚本内保留的历史 # `git remote set-url origin https://oauth2:$TOKEN@...` 仅在设置了 # WORKFLOW env 时才触发;本 job 严禁设置 WORKFLOW,否则 token 会落盘到 # 共享 bare 仓库的 config 并污染所有其它 slot。此处显式重置以固化约束。 git --git-dir="$REPOSITORY_DIR" remote set-url origin "${{ github.server_url }}/${{ github.repository }}.git" if [ ! -f "$WORKTREE_DIR/.git" ]; then exec 9>"$WORKTREE_LOCK" flock 9 if [ ! -f "$WORKTREE_DIR/.git" ]; then if [ -e "$WORKTREE_DIR" ]; then echo "Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2 exit 1 fi mkdir -p "$(dirname "$WORKTREE_DIR")" git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA" fi flock -u 9 fi common_dir=$(git -C "$WORKTREE_DIR" rev-parse --path-format=absolute --git-common-dir) if [ "$common_dir" != "$(realpath "$REPOSITORY_DIR")" ]; then echo "Worktree 不属于共享仓库: $WORKTREE_DIR" >&2 exit 1 fi git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA" git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA" git -C "$WORKTREE_DIR" clean -ffdx actual_sha=$(git -C "$WORKTREE_DIR" rev-parse HEAD) if [ "$actual_sha" != "$HEAD_SHA" ]; then echo "Worktree 提交不匹配: 期望 $HEAD_SHA,实际 $actual_sha" >&2 exit 1 fi echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV" echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV" echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV" echo "Worktree: $WORKTREE_DIR @ $actual_sha" - name: 📥 更新快照并同步 main shell: bash run: | set -euo pipefail cd "$REPO_DIR" echo "========================================" echo "📦 Refresh thirdparty/skill snapshots" echo "========================================" before_ref="" if git show-ref --verify --quiet "refs/remotes/origin/$THIRDPARTY_BRANCH"; then before_ref="$(git rev-parse "origin/$THIRDPARTY_BRANCH")" echo "📌 Previous $THIRDPARTY_BRANCH: $before_ref" else echo "📌 Previous $THIRDPARTY_BRANCH: " fi export MANIFEST_PATH="$MANIFEST_PATH" # BEGIN update_thirdparty_snapshots TARGET_BRANCH="$THIRDPARTY_BRANCH" bash <<'UPDATE_THIRDPARTY' set -euo pipefail REPO_DIR="${REPO_DIR:-$(pwd)}" TARGET_BRANCH="${TARGET_BRANCH:-thirdparty/skill}" MANIFEST_PATH="${MANIFEST_PATH:-.gitea/ci/thirdparty_skills.json}" retry_cmd() { local retries="$1" shift local delay="$1" shift local attempt=1 while true; do if "$@"; then return 0 fi if [ "$attempt" -ge "$retries" ]; then return 1 fi echo "Retry ($attempt/$retries): $*" >&2 sleep "$delay" attempt=$((attempt + 1)) done } github_owner_repo() { case "$1" in https://github.com/*) echo "$1" | sed -E 's#^https://github.com/([^/]+/[^/.]+)(\.git)?$#\1#' ;; http://github.com/*) echo "$1" | sed -E 's#^http://github.com/([^/]+/[^/.]+)(\.git)?$#\1#' ;; git@github.com:*) echo "$1" | sed -E 's#^git@github.com:([^/]+/[^/.]+)(\.git)?$#\1#' ;; *) return 1 ;; esac } resolve_latest_sha() { local repo="$1" local ref="$2" local tmp_json="$3" local gh_repo="$4" local sha="" if [ -n "$gh_repo" ]; then local api_url="https://api.github.com/repos/${gh_repo}/commits/${ref}" if retry_cmd 3 2 curl -fsSL --retry 3 --retry-delay 2 "$api_url" -o "$tmp_json"; then sha="$(sed -n 's/^[[:space:]]*"sha":[[:space:]]*"\([0-9a-f]\{40\}\)".*/\1/p' "$tmp_json" | head -n 1)" if [ -n "$sha" ]; then echo "$sha" return 0 fi fi fi sha="$(retry_cmd 3 2 git -c http.version=HTTP/1.1 ls-remote "$repo" "refs/heads/$ref" | awk 'NR==1 {print $1}')" if [ -n "$sha" ]; then echo "$sha" return 0 fi return 1 } emit_sources_tsv() { python3 - "$MANIFEST_PATH" <<'PY' import json import sys with open(sys.argv[1], encoding="utf-8") as fh: data = json.load(fh) for entry in data["sources"]: print( "\x1f".join( [ entry["id"], entry["upstream_repo"], entry.get("upstream_ref", "main"), entry["snapshot_dir"], entry["sync_mode"], "\x1e".join(entry.get("remove_paths", [])), ] ) ) PY } read_source_metadata_value() { local key="$1" local source_file="$2" if [ ! -f "$source_file" ]; then return 0 fi sed -n "s/^- ${key}:[[:space:]]*//p" "$source_file" | head -n 1 } remove_snapshot_paths() { local snapshot_dir="$1" local remove_paths="$2" [ -n "$remove_paths" ] || return 0 local IFS=$'\x1e' read -r -a paths <<< "$remove_paths" for path in "${paths[@]}"; do [ -n "$path" ] || continue rm -rf "$snapshot_dir/$path" done } cd "$REPO_DIR" git config user.name "$GIT_USER_NAME" git config user.email "$GIT_USER_EMAIL" tmp_dir="$(mktemp -d)" cleanup() { rm -rf "$tmp_dir" } trap cleanup EXIT if [ ! -f "$MANIFEST_PATH" ]; then echo "ERROR: third-party manifest not found: $MANIFEST_PATH" >&2 exit 1 fi manifest_copy="$tmp_dir/thirdparty_skills.json" cp "$MANIFEST_PATH" "$manifest_copy" MANIFEST_PATH="$manifest_copy" git fetch origin "$TARGET_BRANCH" git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH" sources_file="$tmp_dir/sources.tsv" if ! emit_sources_tsv > "$sources_file"; then echo "ERROR: failed to load third-party manifest: $MANIFEST_PATH" >&2 exit 1 fi changed=0 while IFS=$'\x1f' read -r source_id upstream_repo upstream_ref snapshot_dir sync_mode remove_paths; do [ -n "$source_id" ] || continue remove_paths_md="${remove_paths//$'\x1e'/,}" gh_repo="" if gh_repo="$(github_owner_repo "$upstream_repo" 2>/dev/null)"; then : fi latest_sha="$(resolve_latest_sha "$upstream_repo" "$upstream_ref" "$tmp_dir/${source_id}-latest.json" "$gh_repo" || true)" if [ -z "$latest_sha" ]; then echo "ERROR: failed to resolve upstream ref for ${source_id}: $upstream_repo $upstream_ref" >&2 exit 1 fi current_sha="$(read_source_metadata_value "Ref" "$snapshot_dir/SOURCE.md")" current_remove_paths="$(read_source_metadata_value "Remove-Paths" "$snapshot_dir/SOURCE.md")" if [ "$latest_sha" = "$current_sha" ] && [ "$remove_paths_md" = "$current_remove_paths" ]; then echo "Third-party snapshot is up to date for ${source_id}: $latest_sha" continue fi rm -rf "$snapshot_dir" mkdir -p "$snapshot_dir" snapshot_loaded=0 if [ -n "$gh_repo" ]; then tar_url="https://codeload.github.com/${gh_repo}/tar.gz/${latest_sha}" if retry_cmd 3 2 curl -fsSL --retry 3 --retry-delay 2 "$tar_url" -o "$tmp_dir/${source_id}.tar.gz"; then tar -xzf "$tmp_dir/${source_id}.tar.gz" -C "$snapshot_dir" --strip-components=1 snapshot_loaded=1 fi fi if [ "$snapshot_loaded" -eq 0 ]; then upstream_dir="$tmp_dir/${source_id}-upstream" git init "$upstream_dir" >/dev/null git -C "$upstream_dir" remote add origin "$upstream_repo" retry_cmd 3 2 git -C "$upstream_dir" fetch --depth 1 origin "$latest_sha" git -C "$upstream_dir" checkout --detach FETCH_HEAD git -C "$upstream_dir" archive --format=tar HEAD | tar -xf - -C "$snapshot_dir" fi remove_snapshot_paths "$snapshot_dir" "$remove_paths" snapshot_date="$(date -u +%Y-%m-%d)" cat > "$snapshot_dir/SOURCE.md" </ [ -f "skills/$name/SKILL.md" ] } skill_dir_included() { local name="$1" local include_skill_dirs="$2" [ -n "$include_skill_dirs" ] || return 0 local IFS=$'\x1e' local expected read -r -a included <<< "$include_skill_dirs" for expected in "${included[@]}"; do if [ "$name" = "$expected" ]; then return 0 fi done return 1 } cd "$REPO_DIR" git config user.name "$GIT_USER_NAME" git config user.email "$GIT_USER_EMAIL" git fetch origin "$THIRDPARTY_BRANCH" git fetch origin "$TARGET_BRANCH" tmp_dir="$(mktemp -d)" cleanup() { rm -rf "$tmp_dir" } trap cleanup EXIT git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH" mkdir -p "skills/thirdparty/.sources" sources_file="$tmp_dir/sources.tsv" if ! emit_sources_tsv > "$sources_file"; then echo "ERROR: failed to load third-party manifest: $MANIFEST_PATH" >&2 exit 1 fi while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_subdir output_name platform_config template_root data_dir scripts_dir include_skill_dirs; do [ -n "$source_id" ] || continue if [ -f "$source_list" ]; then while IFS= read -r name; do [ -n "$name" ] || continue rm -rf "skills/$name" done < "$source_list" fi done < "$sources_file" declare -A owners=() while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_subdir output_name platform_config template_root data_dir scripts_dir include_skill_dirs; do [ -n "$source_id" ] || continue git archive --format=tar "origin/${THIRDPARTY_BRANCH}" "$snapshot_dir" | tar -xf - -C "$tmp_dir" snapshot_root="$tmp_dir/$snapshot_dir" names=() case "$sync_mode" in copy_skill_dirs) source_skills_dir="$snapshot_root/$skills_subdir" if [ ! -d "$source_skills_dir" ]; then echo "ERROR: $skills_subdir not found in snapshot $snapshot_dir" >&2 exit 1 fi for dir in "$source_skills_dir"/*; do [ -d "$dir" ] || continue name="$(basename "$dir")" if ! skill_dir_included "$name" "$include_skill_dirs"; then continue fi if [ -n "${owners[$name]:-}" ] && [ "${owners[$name]}" != "$source_id" ]; then echo "ERROR: duplicate third-party skill name: $name" >&2 exit 1 fi if tracked_skill_exists "$name"; then echo "ERROR: skill name conflict with tracked skill: $name" >&2 exit 1 fi rm -rf "skills/thirdparty/$name" cp -R "$dir" "skills/thirdparty/$name" names+=("$name") owners["$name"]="$source_id" done ;; render_skill) name="$output_name" if [ -n "${owners[$name]:-}" ] && [ "${owners[$name]}" != "$source_id" ]; then echo "ERROR: duplicate third-party skill name: $name" >&2 exit 1 fi if tracked_skill_exists "$name"; then echo "ERROR: skill name conflict with tracked skill: $name" >&2 exit 1 fi render_skill "$snapshot_root" "skills/thirdparty/$name" "$platform_config" "$template_root" "$data_dir" "$scripts_dir" names+=("$name") owners["$name"]="$source_id" ;; *) echo "ERROR: unsupported sync mode: $sync_mode" >&2 exit 1 ;; esac printf "%s\n" "${names[@]}" | sort > "$source_list" done < "$sources_file" git add skills if git diff --cached --quiet; then echo "No third-party skills to sync." exit 0 fi git commit -m ":package: deps(skills): sync thirdparty skills" TOKEN="${WORKFLOW:-}" if [ -n "$TOKEN" ] && [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ]; then git remote set-url origin "https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" fi git push origin "$TARGET_BRANCH" SYNC_THIRDPARTY # END sync_thirdparty_skills echo "✅ Update and sync finished." - name: 🧹 重置 Worktree if: always() shell: bash run: | # 持久化 worktree 由下次运行前的 detached checkout + reset + clean 复用, # 此处仅在结束时 detach,避免留下已检出的分支阻塞其它 worktree。 if [ -n "${REPO_DIR:-}" ] && [ -f "$REPO_DIR/.git" ]; then git -C "$REPO_DIR" checkout --detach --force "${SOURCE_SHA}" 2>/dev/null || true fi