Skip to content

scan

scan #58

Workflow file for this run

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}"