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" PYTHON_BIN="${PYTHON_BIN:-$(command -v python3.11 || command -v python3)}" "$PYTHON_BIN" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else "Python 3.11 or newer is required")' export PYTHON_BIN 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() { "$PYTHON_BIN" - "$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 # Remove snapshot directories that were managed by an older manifest. # Managed snapshots are identified by their root SOURCE.md marker. declare -A active_snapshots=() while IFS=$'\x1f' read -r source_id upstream_repo upstream_ref snapshot_dir sync_mode remove_paths; do [ -n "$source_id" ] || continue active_snapshots["$snapshot_dir"]=1 done < "$sources_file" changed=0 for source_marker in */SOURCE.md; do [ -f "$source_marker" ] || continue stale_snapshot="${source_marker%/SOURCE.md}" if [ -z "${active_snapshots[$stale_snapshot]:-}" ]; then echo "Removing orphaned third-party snapshot: $stale_snapshot" rm -rf -- "$stale_snapshot" git add -A -- "$stale_snapshot" changed=1 fi done 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" < $after_ref" fi echo "========================================" echo "🔄 Sync skills into main" echo "========================================" git fetch origin main git checkout -B main origin/main # BEGIN sync_thirdparty_skills TARGET_BRANCH="main" \ THIRDPARTY_BRANCH="$THIRDPARTY_BRANCH" \ MANIFEST_PATH="$MANIFEST_PATH" \ bash <<'SYNC_THIRDPARTY' set -euo pipefail REPO_DIR="${REPO_DIR:-$(pwd)}" THIRDPARTY_BRANCH="${THIRDPARTY_BRANCH:-thirdparty/skill}" TARGET_BRANCH="${TARGET_BRANCH:-main}" MANIFEST_PATH="${MANIFEST_PATH:-.gitea/ci/thirdparty_skills.json}" PYTHON_BIN="${PYTHON_BIN:-$(command -v python3.11 || command -v python3)}" "$PYTHON_BIN" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else "Python 3.11 or newer is required")' emit_sources_tsv() { "$PYTHON_BIN" - "$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["snapshot_dir"], entry["sync_mode"], entry["source_list"], "\x1e".join( entry.get( "skills_subdirs", [entry.get("skills_subdir", "")], ) ), entry.get("output_name", entry["id"]), entry.get("platform_config", ""), entry.get("template_root", ""), entry.get("data_dir", ""), entry.get("scripts_dir", ""), "\x1e".join(entry.get("include_skill_dirs", [])), entry.get("overlay_patch", ""), "\x1e".join(entry.get("include_paths", [])), ] ) ) PY } render_skill() { local snapshot_root="$1" local output_dir="$2" local platform_config_rel="$3" local template_root_rel="$4" local data_dir_rel="$5" local scripts_dir_rel="$6" "$PYTHON_BIN" - "$snapshot_root" "$output_dir" "$platform_config_rel" "$template_root_rel" "$data_dir_rel" "$scripts_dir_rel" <<'PY' import json import pathlib import shutil import sys snapshot_root = pathlib.Path(sys.argv[1]) output_dir = pathlib.Path(sys.argv[2]) platform_config_path = snapshot_root / sys.argv[3] template_root = snapshot_root / sys.argv[4] data_dir = snapshot_root / sys.argv[5] scripts_dir = snapshot_root / sys.argv[6] config = json.loads(platform_config_path.read_text(encoding="utf-8")) skill_template = (template_root / "base" / "skill-content.md").read_text(encoding="utf-8") quick_reference = "" if config.get("sections", {}).get("quickReference"): quick_reference = "\n" + (template_root / "base" / "quick-reference.md").read_text(encoding="utf-8") def render_frontmatter(frontmatter): if not frontmatter: return "" lines = ["---"] for key, value in frontmatter.items(): if any(ch in value for ch in ':"\n'): value = value.replace('"', '\\"') lines.append(f'{key}: "{value}"') else: lines.append(f"{key}: {value}") lines.extend(["---", ""]) return "\n".join(lines) content = skill_template content = content.replace("{{TITLE}}", config["title"]) content = content.replace("{{DESCRIPTION}}", config["description"]) content = content.replace("{{SCRIPT_PATH}}", config["scriptPath"]) content = content.replace("{{SKILL_OR_WORKFLOW}}", config["skillOrWorkflow"]) content = content.replace("{{QUICK_REFERENCE}}", quick_reference) if output_dir.exists(): shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) (output_dir / "SKILL.md").write_text( render_frontmatter(config.get("frontmatter")) + content, encoding="utf-8", ) if data_dir.exists(): shutil.copytree(data_dir, output_dir / "data") if scripts_dir.exists(): shutil.copytree(scripts_dir, output_dir / "scripts") PY } tracked_skill_exists() { local name="$1" # conflict only if a first-party SKILL.md exists directly under skills// [ -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 } apply_overlay_patch() { local source_id="$1" local patch_file="$2" [ -n "$patch_file" ] || return 0 if [ ! -f "$patch_file" ]; then echo "ERROR: overlay patch not found for $source_id: $patch_file" >&2 exit 1 fi if ! git apply --check "$patch_file"; then echo "ERROR: overlay patch no longer applies for $source_id: $patch_file" >&2 exit 1 fi git apply "$patch_file" echo "Applied overlay patch for $source_id: $patch_file" } copy_skill_root() { local snapshot_root="$1" local output_dir="$2" local include_paths="$3" "$PYTHON_BIN" - "$snapshot_root" "$output_dir" "$include_paths" <<'PY' import os import pathlib import shutil import sys snapshot_root = pathlib.Path(sys.argv[1]).resolve(strict=True) output_dir = pathlib.Path(sys.argv[2]) raw_paths = [value for value in sys.argv[3].split("\x1e") if value] if not raw_paths: raise SystemExit("ERROR: copy_skill_root requires include_paths") sources = [] seen = set() for value in raw_paths: relative = pathlib.PurePosixPath(value) if ( relative.is_absolute() or not relative.parts or any(part in ("", ".", "..") for part in relative.parts) ): raise SystemExit(f"ERROR: unsafe copy_skill_root include path: {value}") if value in seen: raise SystemExit(f"ERROR: duplicate copy_skill_root include path: {value}") seen.add(value) source_path = snapshot_root / pathlib.Path(*relative.parts) if not source_path.exists(): raise SystemExit( f"ERROR: copy_skill_root include path not found: {value}" ) source = source_path.resolve(strict=True) try: source.relative_to(snapshot_root) except ValueError: raise SystemExit( f"ERROR: copy_skill_root include path escapes snapshot: {value}" ) if not source.is_file() and not source.is_dir(): raise SystemExit( f"ERROR: unsupported copy_skill_root source type: {value}" ) sources.append((relative, source)) if os.path.lexists(output_dir): if output_dir.is_symlink() or output_dir.is_file(): output_dir.unlink() else: shutil.rmtree(output_dir) output_dir.mkdir(parents=True) for relative, source in sources: destination = output_dir.joinpath(*relative.parts) destination.parent.mkdir(parents=True, exist_ok=True) if source.is_dir(): shutil.copytree(source, destination) else: shutil.copy2(source, destination) if not (output_dir / "SKILL.md").is_file(): raise SystemExit( f"ERROR: copy_skill_root output is missing SKILL.md: {output_dir}" ) PY } 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 # Clean skill directories and source lists left by sources removed from # the manifest. Active lists are cleared below before being rewritten. declare -A active_source_lists=() while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_subdirs output_name platform_config template_root data_dir scripts_dir include_skill_dirs overlay_patch include_paths; do [ -n "$source_id" ] || continue active_source_lists["$source_list"]=1 done < "$sources_file" for source_list in skills/thirdparty/.sources/*.list; do [ -f "$source_list" ] || continue if [ -z "${active_source_lists[$source_list]:-}" ]; then echo "Removing orphaned third-party source list: $source_list" while IFS= read -r name; do [ -n "$name" ] || continue rm -rf -- "skills/thirdparty/$name" done < "$source_list" rm -f -- "$source_list" fi done while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_subdirs output_name platform_config template_root data_dir scripts_dir include_skill_dirs overlay_patch include_paths; do [ -n "$source_id" ] || continue if [ -f "$source_list" ]; then while IFS= read -r name; do [ -n "$name" ] || continue rm -rf "skills/thirdparty/$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_subdirs output_name platform_config template_root data_dir scripts_dir include_skill_dirs overlay_patch include_paths; 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) local_ifs="$IFS" IFS=$'\x1e' read -r -a skill_roots <<< "$skills_subdirs" IFS="$local_ifs" for skills_subdir in "${skill_roots[@]}"; do 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 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" ;; copy_skill_root) 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 copy_skill_root "$snapshot_root" "skills/thirdparty/$name" "$include_paths" names+=("$name") owners["$name"]="$source_id" ;; *) echo "ERROR: unsupported sync mode: $sync_mode" >&2 exit 1 ;; esac apply_overlay_patch "$source_id" "$overlay_patch" 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