Skip to content

Track upstream AI-Research-SKILLs releases #11

Track upstream AI-Research-SKILLs releases

Track upstream AI-Research-SKILLs releases #11

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"