v4.7.0 — PM becomes a production toolkit (54 skills, ~30k lines added) #4
Workflow file for this run
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
| # ============================================================================ | |
| # VirusTotal Scan — Malware scanning for release artifacts | |
| # ============================================================================ | |
| # Scans release archives through the VirusTotal API and posts results | |
| # as a comment on the release. Provides a trust signal for users | |
| # downloading skill packages. | |
| # | |
| # Required secret: VIRUSTOTAL_API_KEY (free tier: 4 req/min, 500 req/day) | |
| # ============================================================================ | |
| name: VirusTotal Scan | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag to scan (e.g., v3.0.0)' | |
| required: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| scan: | |
| name: Scan release artifacts | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Determine release tag | |
| id: tag | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create archive for scanning | |
| run: | | |
| TAG="${{ steps.tag.outputs.tag }}" | |
| ARCHIVE="claude-skills-${TAG}.tar.gz" | |
| # Package only skill content (exclude .git, docs, etc.) | |
| tar czf "$ARCHIVE" \ | |
| --exclude='.git' \ | |
| --exclude='documentation' \ | |
| --exclude='__pycache__' \ | |
| --exclude='.DS_Store' \ | |
| . | |
| echo "Created archive: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))" | |
| echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV" | |
| - name: Submit to VirusTotal | |
| id: vt-scan | |
| env: | |
| VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} | |
| run: | | |
| if [ -z "$VT_API_KEY" ]; then | |
| echo "::warning::VIRUSTOTAL_API_KEY not configured. Skipping scan." | |
| echo "skipped=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Uploading $ARCHIVE to VirusTotal..." | |
| RESPONSE=$(curl -s --request POST \ | |
| --url https://www.virustotal.com/api/v3/files \ | |
| --header "x-apikey: ${VT_API_KEY}" \ | |
| --form "file=@${ARCHIVE}") | |
| ANALYSIS_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])" 2>/dev/null || true) | |
| if [ -z "$ANALYSIS_ID" ]; then | |
| echo "::error::Failed to submit file. Response: $RESPONSE" | |
| exit 1 | |
| fi | |
| echo "Analysis ID: $ANALYSIS_ID" | |
| echo "analysis_id=$ANALYSIS_ID" >> "$GITHUB_OUTPUT" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| - name: Wait for analysis and fetch results | |
| if: steps.vt-scan.outputs.skipped != 'true' | |
| env: | |
| VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} | |
| ANALYSIS_ID: ${{ steps.vt-scan.outputs.analysis_id }} | |
| run: | | |
| echo "Waiting for VirusTotal analysis to complete..." | |
| for i in $(seq 1 12); do | |
| sleep 15 | |
| RESULT=$(curl -s \ | |
| --url "https://www.virustotal.com/api/v3/analyses/${ANALYSIS_ID}" \ | |
| --header "x-apikey: ${VT_API_KEY}") | |
| STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['attributes']['status'])" 2>/dev/null || echo "unknown") | |
| if [ "$STATUS" = "completed" ]; then | |
| echo "$RESULT" | python3 -c " | |
| import sys, json | |
| data = json.load(sys.stdin)['data']['attributes']['stats'] | |
| print(f\"Malicious: {data.get('malicious', 0)}\") | |
| print(f\"Suspicious: {data.get('suspicious', 0)}\") | |
| print(f\"Undetected: {data.get('undetected', 0)}\") | |
| print(f\"Harmless: {data.get('harmless', 0)}\") | |
| if data.get('malicious', 0) > 0: | |
| print('::error::VirusTotal detected malicious content!') | |
| sys.exit(1) | |
| print('Scan passed — no threats detected.') | |
| " | |
| exit $? | |
| fi | |
| echo " Attempt $i/12 — status: $STATUS" | |
| done | |
| echo "::warning::Analysis did not complete within 3 minutes." |