name: Sync Skills to Orchestra on: push: branches: - main workflow_dispatch: # Allow manual trigger jobs: sync-skills: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 2 # Fetch last 2 commits to detect changes - name: Detect changed skill folders id: changes run: | # Get list of changed files in last commit CHANGED_FILES=$(git diff --name-only HEAD^..HEAD) echo "Changed files:" echo "$CHANGED_FILES" # Find skill directories - supports two patterns: # Pattern 1: XX-category/skill-name/SKILL.md (nested skills) # Pattern 2: XX-category/SKILL.md (standalone skills like 20-ml-paper-writing) SKILL_DIRS="" # Pattern 1: Nested skills (XX-category/skill-name/) NESTED=$(echo "$CHANGED_FILES" | grep -E '^[0-9]{2}-[^/]+/[^/]+/' | sed -E 's|^([0-9]{2}-[^/]+/[^/]+)/.*|\1|' | sort -u) if [ -n "$NESTED" ]; then SKILL_DIRS="$NESTED" fi # Pattern 2: Standalone skills (XX-category/ with SKILL.md directly inside) STANDALONE=$(echo "$CHANGED_FILES" | grep -E '^[0-9]{2}-[^/]+/SKILL\.md$' | sed -E 's|^([0-9]{2}-[^/]+)/SKILL\.md$|\1|' | sort -u) if [ -n "$STANDALONE" ]; then if [ -n "$SKILL_DIRS" ]; then SKILL_DIRS=$(printf "%s\n%s" "$SKILL_DIRS" "$STANDALONE" | sort -u) else SKILL_DIRS="$STANDALONE" fi fi echo "Changed skill directories:" echo "$SKILL_DIRS" # Convert to JSON array for matrix if [ -z "$SKILL_DIRS" ]; then SKILLS_JSON="[]" SKILL_COUNT=0 else SKILLS_JSON=$(echo "$SKILL_DIRS" | jq -R -s -c 'split("\n") | map(select(length > 0))') SKILL_COUNT=$(echo "$SKILL_DIRS" | grep -c . || echo "0") fi echo "skills=$SKILLS_JSON" >> $GITHUB_OUTPUT echo "count=$SKILL_COUNT" >> $GITHUB_OUTPUT - name: Process and sync skills if: steps.changes.outputs.count > 0 env: ORCHESTRA_API_URL: ${{ secrets.ORCHESTRA_API_URL }} ORCHESTRA_SYNC_API_KEY: ${{ secrets.ORCHESTRA_SYNC_API_KEY }} run: | SKILLS='${{ steps.changes.outputs.skills }}' echo "Processing $(echo $SKILLS | jq 'length') skill(s)..." # Install jq for JSON processing sudo apt-get update && sudo apt-get install -y jq zip # Loop through each skill directory echo "$SKILLS" | jq -r '.[]' | while read SKILL_PATH; do echo "===================================================" echo "Processing: $SKILL_PATH" echo "===================================================" # Check if SKILL.md exists if [ ! -f "$SKILL_PATH/SKILL.md" ]; then echo "⚠️ WARNING: No SKILL.md found in $SKILL_PATH, skipping" continue fi # Extract skill name from SKILL.md frontmatter SKILL_NAME=$(grep -A 20 "^---$" "$SKILL_PATH/SKILL.md" | grep "^name:" | head -1 | sed 's/name: *//;s/"//g;s/'\''//g' | tr -d '\r') # Extract author from SKILL.md frontmatter AUTHOR=$(grep -A 20 "^---$" "$SKILL_PATH/SKILL.md" | grep "^author:" | head -1 | sed 's/author: *//;s/"//g;s/'\''//g' | tr -d '\r') # Default values if [ -z "$SKILL_NAME" ]; then # Extract from directory name as fallback SKILL_NAME=$(basename "$SKILL_PATH") echo "⚠️ No 'name' in frontmatter, using directory name: $SKILL_NAME" fi if [ -z "$AUTHOR" ]; then AUTHOR="Orchestra Research" echo "⚠️ No 'author' in frontmatter, defaulting to: $AUTHOR" fi echo "Skill Name: $SKILL_NAME" echo "Author: $AUTHOR" echo "Path: $SKILL_PATH" # Create temporary directory for zipping TEMP_DIR=$(mktemp -d) SKILL_DIR="$TEMP_DIR/$SKILL_NAME" mkdir -p "$SKILL_DIR" # Copy all contents of skill directory (SKILL.md, references/, scripts/, assets/, etc.) cp -r "$SKILL_PATH"/* "$SKILL_DIR/" 2>/dev/null || true # Create zip file (exclude hidden files and .gitkeep) ZIP_FILE="$TEMP_DIR/${SKILL_NAME}.zip" cd "$TEMP_DIR" zip -r "$ZIP_FILE" "$SKILL_NAME" -x "*/.*" "*/.gitkeep" "*.DS_Store" cd - # Verify zip was created if [ ! -f "$ZIP_FILE" ]; then echo "❌ ERROR: Failed to create zip file for $SKILL_NAME" continue fi echo "✓ Created zip: $(ls -lh "$ZIP_FILE" | awk '{print $5}')" # Write SKILL.md content to temp file (avoid argument length limits) SKILL_MD_FILE="$TEMP_DIR/skill.md" cat "$SKILL_PATH/SKILL.md" > "$SKILL_MD_FILE" # Encode zip to base64 and write to temp file (avoid argument length limits) ZIP_BASE64_FILE="$TEMP_DIR/base64.txt" base64 -w 0 "$ZIP_FILE" > "$ZIP_BASE64_FILE" 2>/dev/null || base64 "$ZIP_FILE" > "$ZIP_BASE64_FILE" # Prepare JSON payload (use --rawfile for large content) JSON_PAYLOAD=$(jq -n \ --arg skillName "$SKILL_NAME" \ --arg skillPath "$SKILL_PATH" \ --arg author "$AUTHOR" \ --rawfile skillMdContent "$SKILL_MD_FILE" \ --rawfile zipBase64 "$ZIP_BASE64_FILE" \ '{ skillName: $skillName, skillPath: $skillPath, author: $author, skillMdContent: $skillMdContent, zipBase64: $zipBase64 }') # Send to Orchestra API (write JSON to file to avoid argument length limits) echo "📤 Uploading to Orchestra..." JSON_FILE="$TEMP_DIR/payload.json" echo "$JSON_PAYLOAD" > "$JSON_FILE" RESPONSE=$(curl -s -w "\n%{http_code}" -L \ -X POST \ -H "Content-Type: application/json" \ -H "X-Admin-API-Key: $ORCHESTRA_SYNC_API_KEY" \ -d @"$JSON_FILE" \ "$ORCHESTRA_API_URL/api/admin/sync-github-skill") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') echo "HTTP Status: $HTTP_CODE" echo "Response: $BODY" if [ "$HTTP_CODE" = "200" ]; then ACTION=$(echo "$BODY" | jq -r '.action // "synced"') SOURCE=$(echo "$BODY" | jq -r '.source // "unknown"') echo "✅ SUCCESS: Skill $SKILL_NAME $ACTION (source: $SOURCE)" else ERROR_MSG=$(echo "$BODY" | jq -r '.error // "Unknown error"') echo "❌ FAILED: $ERROR_MSG" exit 1 fi # Cleanup rm -rf "$TEMP_DIR" echo "" done echo "===================================================" echo "✅ Sync completed successfully!" echo "===================================================" - name: No changes detected if: steps.changes.outputs.count == 0 run: | echo "ℹ️ No skill changes detected in this commit" echo "Only commits that modify skill directories will trigger sync"