Track upstream AI-Research-SKILLs releases #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Track upstream AI-Research-SKILLs releases | |
| on: | |
| schedule: | |
| - cron: '0 9 * * 1' # Weekly Monday 9:00 UTC | |
| workflow_dispatch: # Manual trigger | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: update-ai-research-skills | |
| cancel-in-progress: false | |
| jobs: | |
| check-and-update: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get latest upstream release | |
| id: upstream | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| RELEASE=$(gh api repos/Orchestra-Research/AI-Research-SKILLs/releases/latest \ | |
| --jq '{tag: .tag_name, url: .html_url, name: .name, body: .body}') | |
| TAG=$(echo "$RELEASE" | jq -r '.tag') | |
| URL=$(echo "$RELEASE" | jq -r '.url') | |
| NAME=$(echo "$RELEASE" | jq -r '.name') | |
| BODY=$(echo "$RELEASE" | jq -r '.body' | head -c 2000) | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "url=$URL" >> "$GITHUB_OUTPUT" | |
| echo "name=$NAME" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "body<<RELEASE_EOF" | |
| echo "$BODY" | |
| echo "RELEASE_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history required for git subtree | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check current tracked version | |
| id: current | |
| run: | | |
| VERSION_FILE="vendor/ai-research-skills/.tracked-version" | |
| if [ -f "$VERSION_FILE" ]; then | |
| CURRENT=$(cat "$VERSION_FILE" | tr -d '[:space:]') | |
| else | |
| CURRENT="none" | |
| fi | |
| echo "version=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "Current tracked version: $CURRENT" | |
| echo "Latest upstream release: ${{ steps.upstream.outputs.tag }}" | |
| - name: Skip if already up to date | |
| if: steps.current.outputs.version == steps.upstream.outputs.tag | |
| run: echo "Already at ${{ steps.upstream.outputs.tag }}, nothing to do." | |
| - name: Pull upstream release via subtree | |
| if: steps.current.outputs.version != steps.upstream.outputs.tag | |
| id: pull | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| TAG="${{ steps.upstream.outputs.tag }}" | |
| BRANCH="chore/update-ai-research-skills-${TAG}" | |
| # Use -B to handle reruns where the branch may already exist locally | |
| git checkout -B "$BRANCH" | |
| PULL_LOG=$(mktemp) | |
| if git subtree pull --prefix=vendor/ai-research-skills \ | |
| https://github.com/Orchestra-Research/AI-Research-SKILLs.git "$TAG" \ | |
| --squash -m "chore: update AI-Research-SKILLs to ${TAG}" 2>"$PULL_LOG"; then | |
| echo "status=ok" >> "$GITHUB_OUTPUT" | |
| else | |
| # Distinguish merge conflict from other failures | |
| if git ls-files --unmerged | grep -q .; then | |
| echo "status=conflict" >> "$GITHUB_OUTPUT" | |
| git merge --abort 2>/dev/null || true | |
| else | |
| echo "Subtree pull failed (not a merge conflict):" | |
| cat "$PULL_LOG" | |
| rm -f "$PULL_LOG" | |
| exit 1 | |
| fi | |
| fi | |
| rm -f "$PULL_LOG" | |
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | |
| # ── Post-pull cleanup ────────────────────────────────────────── | |
| - name: Clean up excluded content | |
| if: steps.pull.outputs.status == 'ok' | |
| run: | | |
| rm -rf vendor/ai-research-skills/.claude-plugin | |
| rm -rf vendor/ai-research-skills/0-autoresearch-skill | |
| rm -rf vendor/ai-research-skills/.github | |
| rm -rf vendor/ai-research-skills/demos | |
| rm -rf vendor/ai-research-skills/dev_data | |
| rm -rf vendor/ai-research-skills/video-promo | |
| rm -rf vendor/ai-research-skills/packages | |
| rm -rf vendor/ai-research-skills/anthropic_official_docs | |
| rm -rf vendor/ai-research-skills/docs | |
| rm -f vendor/ai-research-skills/package.json | |
| rm -f vendor/ai-research-skills/CLAUDE.md | |
| rm -f vendor/ai-research-skills/CONTRIBUTING.md | |
| rm -f vendor/ai-research-skills/WELCOME.md | |
| rm -f vendor/ai-research-skills/.gitignore | |
| echo "${{ steps.upstream.outputs.tag }}" > vendor/ai-research-skills/.tracked-version | |
| git add -A | |
| git diff --staged --quiet || \ | |
| git commit -m "chore: clean subtree artifacts, track ${{ steps.upstream.outputs.tag }}" | |
| # ── Compatibility check ──────────────────────────────────────── | |
| - name: Check for breaking changes | |
| if: steps.pull.outputs.status == 'ok' | |
| id: compat | |
| run: | | |
| ERRORS="" | |
| NEW_CATEGORIES="" | |
| # 1. Check each expected numbered category dir (01-21) still exists. | |
| # Vendor skills are not registered in marketplace.json anymore — | |
| # only /research is exposed and the router loads them via Read. | |
| # So validation walks the vendor tree directly. | |
| for num in $(seq -w 1 21); do | |
| DIR=$(ls -d vendor/ai-research-skills/${num}-* 2>/dev/null | head -1) | |
| if [ -z "$DIR" ]; then | |
| ERRORS="${ERRORS}- **MISSING CATEGORY**: directory \`${num}-*\` not found in upstream\n" | |
| fi | |
| done | |
| # 2. Detect *new* categories that the router doesn't yet know about — | |
| # these need manual keyword curation in skill-router.md. | |
| for cat_dir in vendor/ai-research-skills/[0-2][0-9]-*/; do | |
| CATEGORY_DIR=$(basename "${cat_dir%/}") | |
| NUM=$(echo "$CATEGORY_DIR" | grep -oP '^\d+') | |
| if ! grep -qP "^\| ${NUM#0} \|" skills/research/phases/skill-router.md 2>/dev/null && \ | |
| ! grep -qP "^\| ${NUM} \|" skills/research/phases/skill-router.md 2>/dev/null; then | |
| NEW_CATEGORIES="${NEW_CATEGORIES}- **\`${CATEGORY_DIR}\`** — not in skill-router.md. Needs a new row with trigger keywords.\n" | |
| fi | |
| done | |
| if [ -n "$ERRORS" ]; then | |
| echo "status=breaking" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "status=ok" >> "$GITHUB_OUTPUT" | |
| fi | |
| [ -n "$NEW_CATEGORIES" ] && echo "has_new_categories=true" >> "$GITHUB_OUTPUT" || echo "has_new_categories=false" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "errors<<CHECK_EOF" | |
| echo -e "$ERRORS" | |
| echo "CHECK_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "new_categories<<CAT_EOF" | |
| echo -e "$NEW_CATEGORIES" | |
| echo "CAT_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| # ── Sync skill-router.md + rebuild .skill-manifest.json ──────── | |
| # sync-skill-router.py writes BOTH in one shot: | |
| # - skill-router.md: refreshed "Available Skills" column + Trigger Keywords | |
| # - vendor/ai-research-skills/.skill-manifest.json: {name → path} lookup | |
| # used by the router at runtime (replaces the former Glob/Read scan). | |
| - name: Sync skill-router and manifest | |
| if: steps.pull.outputs.status == 'ok' && steps.compat.outputs.status == 'ok' | |
| id: router_sync | |
| run: | | |
| python3 .github/scripts/sync-skill-router.py --write | |
| if git diff --quiet skills/research/phases/skill-router.md vendor/ai-research-skills/.skill-manifest.json 2>/dev/null; then | |
| echo "synced=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "synced=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── Commit router + manifest changes ─────────────────────────── | |
| - name: Commit router and manifest sync | |
| if: steps.pull.outputs.status == 'ok' && steps.compat.outputs.status == 'ok' | |
| run: | | |
| git add -A | |
| git diff --staged --quiet || \ | |
| git commit -m "chore: sync skill-router keywords and rebuild skill manifest" | |
| # ── Push and create PR ───────────────────────────────────────── | |
| - name: Push branch and create PR | |
| if: steps.pull.outputs.status == 'ok' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| BRANCH="${{ steps.pull.outputs.branch }}" | |
| git push --force-with-lease origin "$BRANCH" | |
| COMPAT="${{ steps.compat.outputs.status }}" | |
| TAG="${{ steps.upstream.outputs.tag }}" | |
| FROM="${{ steps.current.outputs.version }}" | |
| HAS_NEW_CAT="${{ steps.compat.outputs.has_new_categories }}" | |
| ROUTER_SYNCED="${{ steps.router_sync.outputs.synced }}" | |
| TITLE="chore(deps): update AI-Research-SKILLs ${FROM} → ${TAG}" | |
| RELEASE_NAME="${{ steps.upstream.outputs.name }}" | |
| RELEASE_URL="${{ steps.upstream.outputs.url }}" | |
| RELEASE_BODY="${{ steps.upstream.outputs.body }}" | |
| # Build PR body in a temp file to avoid shell injection | |
| BODY_FILE=$(mktemp) | |
| { | |
| echo "## Update AI-Research-SKILLs: ${FROM} → ${TAG}" | |
| echo "" | |
| echo "**Release**: [${RELEASE_NAME}](${RELEASE_URL})" | |
| echo "" | |
| echo "### Upstream Release Notes" | |
| echo "${RELEASE_BODY}" | |
| echo "" | |
| echo "---" | |
| } > "$BODY_FILE" | |
| LABELS="automated,dependencies" | |
| if [ "$COMPAT" = "breaking" ]; then | |
| { | |
| echo "" | |
| echo "### Compatibility: 🔴 Breaking changes — manual intervention required" | |
| echo "" | |
| echo "${{ steps.compat.outputs.errors }}" | |
| echo "" | |
| echo '> **Do not merge** without fixing `skill-router.md` and the vendor tree.' | |
| } >> "$BODY_FILE" | |
| LABELS="automated,dependencies,breaking-change" | |
| else | |
| echo -e "\n### Compatibility: ✅ All expected category dirs present" >> "$BODY_FILE" | |
| if [ "$HAS_NEW_CAT" = "true" ]; then | |
| { | |
| echo "" | |
| echo "### 🟡 New categories detected — manual intervention required" | |
| echo "${{ steps.compat.outputs.new_categories }}" | |
| echo "" | |
| echo '> A new category needs a new row in `skill-router.md` with **trigger keywords** for paper routing. Vendor skills are no longer registered individually in `marketplace.json` — only `/research` is exposed.' | |
| } >> "$BODY_FILE" | |
| LABELS="${LABELS},new-category" | |
| fi | |
| if [ "$ROUTER_SYNCED" = "true" ]; then | |
| { | |
| echo "" | |
| echo "### Router + manifest synced" | |
| echo '`skill-router.md` (trigger keywords, Available Skills) and `vendor/ai-research-skills/.skill-manifest.json` (name → path lookup) updated from upstream SKILL.md frontmatter.' | |
| } >> "$BODY_FILE" | |
| fi | |
| if [ "$HAS_NEW_CAT" != "true" ] && [ "$ROUTER_SYNCED" != "true" ]; then | |
| echo -e "\nNo structural changes. Content updates only. Safe to merge directly." >> "$BODY_FILE" | |
| fi | |
| fi | |
| # Create or update PR | |
| EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty' 2>/dev/null || true) | |
| # All labels we manage — used to clean stale ones on update | |
| MANAGED_LABELS="automated,dependencies,breaking-change,new-category" | |
| if [ -n "$EXISTING_PR" ]; then | |
| # Remove managed labels that are no longer applicable | |
| CURRENT_LABELS=$(gh pr view "$EXISTING_PR" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || true) | |
| for label in $(echo "$MANAGED_LABELS" | tr ',' ' '); do | |
| if echo ",$CURRENT_LABELS," | grep -q ",$label," && ! echo ",$LABELS," | grep -q ",$label,"; then | |
| gh pr edit "$EXISTING_PR" --remove-label "$label" 2>/dev/null || true | |
| fi | |
| done | |
| gh pr edit "$EXISTING_PR" \ | |
| --title "$TITLE" \ | |
| --body-file "$BODY_FILE" \ | |
| --add-label "$LABELS" | |
| echo "Updated existing PR #${EXISTING_PR}" | |
| else | |
| gh pr create \ | |
| --title "$TITLE" \ | |
| --body-file "$BODY_FILE" \ | |
| --label "$LABELS" \ | |
| --head "$BRANCH" \ | |
| --base master | |
| fi | |
| rm -f "$BODY_FILE" | |
| # ── Handle merge conflicts ───────────────────────────────────── | |
| - name: Open issue on conflict | |
| if: steps.pull.outputs.status == 'conflict' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ steps.upstream.outputs.tag }}" | |
| URL="${{ steps.upstream.outputs.url }}" | |
| ISSUE_TITLE="⚠️ AI-Research-SKILLs subtree conflict at ${TAG}" | |
| # Check if an issue for this tag already exists | |
| EXISTING_ISSUE=$(gh issue list --search "\"${ISSUE_TITLE}\" in:title" --state open --json number --jq '.[0].number // empty' 2>/dev/null || true) | |
| if [ -n "$EXISTING_ISSUE" ]; then | |
| gh issue comment "$EXISTING_ISSUE" --body "Workflow re-ran and the conflict for ${TAG} still exists. No new issue created." | |
| echo "Commented on existing issue #${EXISTING_ISSUE}" | |
| exit 0 | |
| fi | |
| ISSUE_FILE=$(mktemp) | |
| { | |
| echo "Automated \`git subtree pull\` for [${TAG}](${URL}) failed with merge conflicts." | |
| echo "" | |
| echo "### Manual resolution steps" | |
| echo "" | |
| echo "\`\`\`bash" | |
| echo "git subtree pull --prefix=vendor/ai-research-skills \\" | |
| echo " https://github.com/Orchestra-Research/AI-Research-SKILLs.git ${TAG} --squash" | |
| echo "" | |
| echo "# Resolve conflicts, then clean up:" | |
| echo "rm -rf vendor/ai-research-skills/.claude-plugin" | |
| echo "rm -rf vendor/ai-research-skills/0-autoresearch-skill" | |
| echo "rm -rf vendor/ai-research-skills/.github" | |
| echo "rm -rf vendor/ai-research-skills/demos" | |
| echo "rm -rf vendor/ai-research-skills/dev_data" | |
| echo "rm -rf vendor/ai-research-skills/video-promo" | |
| echo "rm -rf vendor/ai-research-skills/packages" | |
| echo "rm -rf vendor/ai-research-skills/anthropic_official_docs" | |
| echo "rm -rf vendor/ai-research-skills/docs" | |
| echo "rm -f vendor/ai-research-skills/package.json vendor/ai-research-skills/CLAUDE.md" | |
| echo "rm -f vendor/ai-research-skills/CONTRIBUTING.md vendor/ai-research-skills/WELCOME.md" | |
| echo "rm -f vendor/ai-research-skills/.gitignore" | |
| echo "echo '${TAG}' > vendor/ai-research-skills/.tracked-version" | |
| echo "" | |
| echo "# Re-run router + manifest sync:" | |
| echo "python3 .github/scripts/sync-skill-router.py --write" | |
| echo "\`\`\`" | |
| echo "" | |
| echo "After resolving, push to a branch and open a PR, or re-trigger this workflow." | |
| } > "$ISSUE_FILE" | |
| gh issue create \ | |
| --title "$ISSUE_TITLE" \ | |
| --body-file "$ISSUE_FILE" \ | |
| --label "automated,dependencies,conflict" | |
| rm -f "$ISSUE_FILE" |