scan #56
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: scan | |
| on: | |
| schedule: | |
| # Every 6h at :17 to dodge top-of-hour cron drift. | |
| - cron: "17 */6 * * *" | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "config.yaml" | |
| - "scanner/**" | |
| - ".github/workflows/scan.yaml" | |
| permissions: {} | |
| concurrency: | |
| group: scan | |
| cancel-in-progress: false | |
| jobs: | |
| enumerate: | |
| name: Enumerate skill sources | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| matrix: ${{ steps.list.outputs.matrix }} | |
| catalogue_sha: ${{ steps.list.outputs.catalogue_sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| - name: Enumerate | |
| id: list | |
| run: scanner enumerate --github-output >> "$GITHUB_OUTPUT" | |
| scan: | |
| name: Scan ${{ matrix.namespace }}/${{ matrix.slug }} | |
| needs: enumerate | |
| if: needs.enumerate.outputs.matrix != '{"include":[]}' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.enumerate.outputs.matrix) }} | |
| steps: | |
| - name: Checkout scanner | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install scanner | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| - name: Install SkillSpector | |
| run: pip install "$(python -c 'import yaml; print(yaml.safe_load(open("config.yaml"))["scanners"]["skillspector"]["pin"])')" | |
| - name: Checkout source repo | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: ${{ matrix.source_repo }} | |
| ref: ${{ matrix.source_ref }} | |
| path: source | |
| persist-credentials: false | |
| - name: Resolve source SHA | |
| id: source_sha | |
| run: | | |
| set -euo pipefail | |
| sha="$(git -C source rev-parse HEAD)" | |
| echo "sha=${sha}" >> "$GITHUB_OUTPUT" | |
| - name: Verify skill path exists | |
| id: path_check | |
| run: | | |
| set -euo pipefail | |
| if [[ -d "source/${{ matrix.skill_path }}" ]]; then | |
| echo "drift=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "drift=true" >> "$GITHUB_OUTPUT" | |
| echo "Skill path source/${{ matrix.skill_path }} not present upstream; will report catalogue drift." >&2 | |
| fi | |
| - name: Determine LLM mode | |
| id: llm | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then | |
| echo "extra_flags=" >> "$GITHUB_OUTPUT" | |
| echo "SkillSpector LLM mode: enabled (anthropic provider, api.anthropic.com)." >&2 | |
| else | |
| echo "extra_flags=--no-llm" >> "$GITHUB_OUTPUT" | |
| echo "::warning::ANTHROPIC_API_KEY secret not set; SkillSpector will run with --no-llm. Set the secret on this repo to enable the LLM semantic pass." | |
| fi | |
| - name: SkillSpector (JSON) | |
| if: steps.path_check.outputs.drift == 'false' | |
| continue-on-error: true | |
| env: | |
| SKILLSPECTOR_PROVIDER: anthropic | |
| SKILLSPECTOR_MODEL: claude-sonnet-4-6 | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| mkdir -p out | |
| skillspector scan "source/${{ matrix.skill_path }}" \ | |
| ${{ steps.llm.outputs.extra_flags }} \ | |
| --format json \ | |
| --output "out/skillspector.json" || true | |
| - name: SkillSpector (SARIF) | |
| if: steps.path_check.outputs.drift == 'false' | |
| continue-on-error: true | |
| env: | |
| SKILLSPECTOR_PROVIDER: anthropic | |
| SKILLSPECTOR_MODEL: claude-sonnet-4-6 | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| mkdir -p out | |
| skillspector scan "source/${{ matrix.skill_path }}" \ | |
| ${{ steps.llm.outputs.extra_flags }} \ | |
| --format sarif \ | |
| --output "out/skillspector.sarif" || true | |
| - name: Combine | |
| run: | | |
| mkdir -p out | |
| scanner combine \ | |
| --namespace "${{ matrix.namespace }}" \ | |
| --slug "${{ matrix.slug }}" \ | |
| --source-repo "${{ matrix.source_repo }}" \ | |
| --source-ref "${{ matrix.source_ref }}" \ | |
| --source-sha "${{ steps.source_sha.outputs.sha }}" \ | |
| --skill-path "${{ matrix.skill_path }}" \ | |
| ${{ steps.path_check.outputs.drift == 'true' && '--catalogue-drift' || '' }} \ | |
| --skillspector-json out/skillspector.json \ | |
| --output out/skill.json | |
| - name: Upload skill artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: skill-${{ matrix.namespace }}-${{ matrix.slug }} | |
| path: out/ | |
| retention-days: 90 | |
| index: | |
| name: Build latest.json | |
| needs: [enumerate, scan] | |
| if: always() && needs.enumerate.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout scanner | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| - name: Download all skill artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: scans | |
| pattern: skill-* | |
| - name: Aggregate | |
| env: | |
| SCANNER_CATALOGUE_SHA: ${{ needs.enumerate.outputs.catalogue_sha }} | |
| run: scanner aggregate scans --output latest.json | |
| - name: Show summary | |
| run: | | |
| python -c "import json; r=json.load(open('latest.json')); print(json.dumps(r['summary'], indent=2))" | |
| - name: Upload scan-index artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: scan-index | |
| path: latest.json | |
| retention-days: 90 | |
| publish-release: | |
| name: Publish GitHub Release | |
| needs: index | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Download scan-index | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: scan-index | |
| path: . | |
| - name: Download all skill artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: skills | |
| pattern: skill-* | |
| - name: Stage release assets | |
| id: stage | |
| run: | | |
| set -euo pipefail | |
| stamp="$(date -u +%Y-%m-%dT%H-%MZ)" | |
| tag="scan-${stamp}" | |
| mkdir -p release-assets | |
| cp latest.json release-assets/ | |
| # Each per-skill artifact is one directory under skills/. | |
| shopt -s nullglob | |
| for d in skills/skill-*; do | |
| base="$(basename "$d")" | |
| if [[ -f "${d}/skillspector.sarif" ]]; then | |
| cp "${d}/skillspector.sarif" "release-assets/${base}.sarif" | |
| fi | |
| if [[ -f "${d}/skillspector.json" ]]; then | |
| cp "${d}/skillspector.json" "release-assets/${base}.skillspector.json" | |
| fi | |
| if [[ -f "${d}/skill.json" ]]; then | |
| cp "${d}/skill.json" "release-assets/${base}.skill.json" | |
| fi | |
| done | |
| ls -la release-assets/ | |
| echo "tag=${tag}" >> "$GITHUB_OUTPUT" | |
| echo "stamp=${stamp}" >> "$GITHUB_OUTPUT" | |
| - name: Create timestamped release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.stage.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "${TAG}" \ | |
| --title "Scan ${TAG}" \ | |
| --notes "Automated scan run. See https://coder.github.io/coder-skill-scanner/latest.json for the public report." \ | |
| release-assets/* | |
| - name: Update rolling latest tag | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| gh release delete latest --yes --cleanup-tag 2>/dev/null || true | |
| gh release create latest \ | |
| --title "Latest scan" \ | |
| --notes "Rolling pointer to the most recent scan. Updated automatically." \ | |
| release-assets/latest.json | |
| publish-pages: | |
| name: Publish to GitHub Pages | |
| needs: index | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pages: write | |
| id-token: write | |
| contents: read | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deploy.outputs.page_url }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| version: 10.34.4 | |
| - name: Set up Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "22" | |
| cache: "pnpm" | |
| cache-dependency-path: site/pnpm-lock.yaml | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install scanner | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| - name: Download scan-index | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: scan-index | |
| path: . | |
| - name: Build SPA | |
| working-directory: site | |
| run: | | |
| pnpm install --frozen-lockfile | |
| pnpm build | |
| - name: Restore prior history from live Pages | |
| env: | |
| PAGES_URL: ${{ vars.PAGES_URL }} | |
| run: python -m scanner._carry_history | |
| - name: Build pages tree | |
| run: | | |
| set -euo pipefail | |
| stamp="$(date -u +%Y-%m-%dT%H-%MZ)" | |
| today="$(date -u +%Y-%m-%d)" | |
| # Start with the React SPA build. | |
| mkdir -p pages | |
| cp -r site/dist/. pages/ | |
| # Drop in static data the SPA fetches at runtime. | |
| cp latest.json pages/latest.json | |
| cp schema/report.schema.json pages/schema.json | |
| # Carry forward prior snapshots so the history page stays continuous. | |
| if [[ -d prior-history ]]; then | |
| mkdir -p pages/history | |
| cp -r prior-history/. pages/history/ 2>/dev/null || true | |
| rm -f pages/history/index.json | |
| fi | |
| # Add the new snapshot. | |
| mkdir -p "pages/history/${today}" | |
| cp latest.json "pages/history/${today}/${stamp}.json" | |
| # Regenerate the manifest so the React app sees every retained run. | |
| scanner index-history pages/history --output pages/history/index.json | |
| # Build the stable v1 API surface (skills.json, per-skill detail, | |
| # badge endpoints, history.json). Derive the public base URL from | |
| # the runtime publishing context so forks get the right prefix | |
| # automatically; the Vite build uses the same logic. | |
| repo_short="${GITHUB_REPOSITORY#*/}" | |
| public_base_url="https://${GITHUB_REPOSITORY_OWNER}.github.io/${repo_short}" | |
| scanner build-api-v1 latest.json \ | |
| --output pages/api/v1 \ | |
| --public-base-url "${public_base_url}" \ | |
| --history-index pages/history/index.json | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 | |
| with: | |
| path: pages | |
| - name: Deploy Pages | |
| id: deploy | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 | |
| open-issue-on-failure: | |
| name: Open or update tracker issue | |
| needs: [enumerate, scan, index, publish-release, publish-pages] | |
| if: failure() && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Open or update tracker issue | |
| uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| filename: .github/ISSUE_TEMPLATE/scanner-down.md | |
| update_existing: true | |
| search_existing: open | |
| notify-slack-on-failure: | |
| name: Slack notification on failure | |
| needs: [enumerate, scan, index, publish-release, publish-pages] | |
| if: failure() && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Post to Slack | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${SLACK_WEBHOOK_URL:-}" ]]; then | |
| echo "SLACK_WEBHOOK_URL secret not set; skipping Slack notification." | |
| exit 0 | |
| fi | |
| curl --fail-with-body --silent --show-error \ | |
| -X POST -H "Content-Type: application/json" \ | |
| -d "{\"text\":\":rotating_light: coder-skill-scanner run failed: ${WORKFLOW_URL}\"}" \ | |
| "${SLACK_WEBHOOK_URL}" |