chore(deps): update anthropics/skills digest to d230a6d #205
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: Build Skill Artifacts | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - 'skills/**/*.yaml' | |
| - 'cmd/dockhand/**' | |
| - 'internal/skills/**' | |
| - 'scripts/skill-scan/**' | |
| - '.github/workflows/build-skills.yml' | |
| - '.github/workflows/skill-scan-report.yml' | |
| - 'go.mod' | |
| - 'go.sum' | |
| pull_request: | |
| branches: [ main ] | |
| paths: | |
| - 'skills/**/*.yaml' | |
| - 'cmd/dockhand/**' | |
| - 'internal/skills/**' | |
| - 'scripts/skill-scan/**' | |
| - '.github/workflows/build-skills.yml' | |
| - '.github/workflows/skill-scan-report.yml' | |
| - 'go.mod' | |
| - 'go.sum' | |
| workflow_dispatch: | |
| permissions: {} | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| discover-skill-configs: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| changed-configs: ${{ steps.find-configs.outputs.changed-configs }} | |
| scan-configs: ${{ steps.find-configs.outputs.scan-configs }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Find skill configurations to build | |
| id: find-configs | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| # Find all skill spec.yaml files | |
| all_configs=$(find skills -name "spec.yaml" -type f 2>/dev/null | sort) | |
| if [ -z "$all_configs" ]; then | |
| echo "No skill configurations found" | |
| echo "changed-configs=[]" >> $GITHUB_OUTPUT | |
| echo "scan-configs=[]" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| if [ "$EVENT_NAME" == "workflow_dispatch" ]; then | |
| configs_to_build="$all_configs" | |
| configs_to_scan="$all_configs" | |
| echo "Manual trigger - building all skill configurations" | |
| elif [ "$EVENT_NAME" == "pull_request" ]; then | |
| changed_files=$(git diff --name-only origin/"$BASE_REF"...HEAD) | |
| configs_to_build="" | |
| configs_to_scan="" | |
| # Scan only specs that actually changed; build the superset below. | |
| for config in $all_configs; do | |
| config_dir=$(dirname "$config") | |
| if echo "$changed_files" | grep -q "^$config$" || echo "$changed_files" | grep -q "^$config_dir/"; then | |
| configs_to_build="$configs_to_build$config"$'\n' | |
| configs_to_scan="$configs_to_scan$config"$'\n' | |
| fi | |
| done | |
| # If core build inputs changed, rebuild all skills — but only scan specs that truly changed. | |
| if echo "$changed_files" | grep -E "(cmd/dockhand/|internal/skills/|scripts/skill-scan/|\.github/workflows/build-skills\.yml|go\.mod|go\.sum)"; then | |
| echo "Core files changed - rebuilding all skill configurations (scan only changed specs)" | |
| configs_to_build="$all_configs" | |
| fi | |
| else | |
| changed_files=$(git diff --name-only HEAD~1..HEAD) | |
| configs_to_build="" | |
| configs_to_scan="" | |
| for config in $all_configs; do | |
| config_dir=$(dirname "$config") | |
| if echo "$changed_files" | grep -q "^$config$" || echo "$changed_files" | grep -q "^$config_dir/"; then | |
| configs_to_build="$configs_to_build$config"$'\n' | |
| configs_to_scan="$configs_to_scan$config"$'\n' | |
| fi | |
| done | |
| if echo "$changed_files" | grep -E "(cmd/dockhand/|internal/skills/|scripts/skill-scan/|\.github/workflows/build-skills\.yml|go\.mod|go\.sum)"; then | |
| echo "Core files changed - rebuilding all skill configurations (scan only changed specs)" | |
| configs_to_build="$all_configs" | |
| fi | |
| fi | |
| configs_json=$(echo "$configs_to_build" | grep -v '^$' | jq -R -s -c 'split("\n")[:-1]') | |
| scan_configs_json=$(echo "$configs_to_scan" | grep -v '^$' | jq -R -s -c 'split("\n")[:-1]') | |
| echo "changed-configs=$configs_json" >> $GITHUB_OUTPUT | |
| echo "scan-configs=$scan_configs_json" >> $GITHUB_OUTPUT | |
| echo "Skill configurations to build: $configs_json" | |
| echo "Skill configurations to scan: $scan_configs_json" | |
| validate-skills: | |
| needs: discover-skill-configs | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.discover-skill-configs.outputs.changed-configs != '[]' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-skill-configs.outputs.changed-configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| - name: Build dockhand | |
| run: go build -o /tmp/dockhand ./cmd/dockhand | |
| - name: Validate skill | |
| env: | |
| CONFIG_FILE: ${{ matrix.config }} | |
| run: | | |
| echo "Validating skill: $CONFIG_FILE" | |
| /tmp/dockhand validate-skill --config "$CONFIG_FILE" | |
| skill-security-scan: | |
| needs: discover-skill-configs | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| if: ${{ needs.discover-skill-configs.outputs.scan-configs != '[]' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-skill-configs.outputs.scan-configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install yq | |
| uses: mikefarah/yq@751d8ad57b84f1794661bc70c0afb92a22ad7b3c # v4.53.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| id: install-deps | |
| run: | | |
| uv pip install --system -r scripts/skill-scan/requirements.txt | |
| skill-scanner --version || true | |
| SCANNER_VERSION=$(uv pip show cisco-ai-skill-scanner | grep "^Version:" | cut -d' ' -f2) | |
| echo "scanner_version=$SCANNER_VERSION" >> $GITHUB_OUTPUT | |
| echo "Installed skill-scanner version: $SCANNER_VERSION" | |
| - name: Extract metadata from config | |
| id: meta | |
| env: | |
| CONFIG_FILE: ${{ matrix.config }} | |
| run: | | |
| skill_name=$(echo "$CONFIG_FILE" | cut -d'/' -f2) | |
| echo "skill_name=$skill_name" >> $GITHUB_OUTPUT | |
| echo "config_file=$CONFIG_FILE" >> $GITHUB_OUTPUT | |
| repository=$(yq '.spec.repository' "$CONFIG_FILE") | |
| ref=$(yq '.spec.ref' "$CONFIG_FILE") | |
| skill_path=$(yq '.spec.path // ""' "$CONFIG_FILE") | |
| echo "repository=$repository" >> $GITHUB_OUTPUT | |
| echo "ref=$ref" >> $GITHUB_OUTPUT | |
| echo "skill_path=$skill_path" >> $GITHUB_OUTPUT | |
| - name: Check out skill source | |
| id: skill-src | |
| env: | |
| SKILL_REPO: ${{ steps.meta.outputs.repository }} | |
| SKILL_REF: ${{ steps.meta.outputs.ref }} | |
| SKILL_PATH: ${{ steps.meta.outputs.skill_path }} | |
| SKILL_NAME: ${{ steps.meta.outputs.skill_name }} | |
| run: | | |
| workdir="/tmp/skill-scan-${SKILL_NAME}" | |
| rm -rf "$workdir" | |
| mkdir -p "$workdir" | |
| git clone --filter=tree:0 --no-checkout --quiet "$SKILL_REPO" "$workdir/repo" | |
| git -C "$workdir/repo" checkout --quiet "$SKILL_REF" | |
| if [ -n "$SKILL_PATH" ]; then | |
| src_dir="$workdir/repo/$SKILL_PATH" | |
| else | |
| src_dir="$workdir/repo" | |
| fi | |
| if [ ! -d "$src_dir" ]; then | |
| echo "Error: skill source directory not found at $src_dir" >&2 | |
| exit 1 | |
| fi | |
| echo "source_dir=$src_dir" >> $GITHUB_OUTPUT | |
| - name: Run skill security scan | |
| id: scan | |
| env: | |
| # LLM analyzer (LiteLLM-driven). Reuses the same provider key as the | |
| # MCP scanner — single secret across both pipelines, single rotation. | |
| # Toggle via repo variable SKILL_SCANNER_USE_LLM=false to disable in a pinch. | |
| SKILL_SCANNER_USE_LLM: ${{ vars.SKILL_SCANNER_USE_LLM || 'true' }} | |
| SKILL_SCANNER_LLM_API_KEY: ${{ secrets.MCP_SCANNER_LLM_API_KEY }} | |
| # Dedicated model knob — set to e.g. anthropic/claude-sonnet-4. If unset, | |
| # skill-scanner falls back to its built-in default (claude-3-5-sonnet-...). | |
| SKILL_SCANNER_LLM_MODEL: ${{ vars.SKILL_SCANNER_LLM_MODEL }} | |
| # Optional consensus voting — N>1 multiplies LLM cost per scan. | |
| SKILL_SCANNER_LLM_CONSENSUS_RUNS: ${{ vars.SKILL_SCANNER_LLM_CONSENSUS_RUNS }} | |
| # Severity threshold for blocking. Findings below this severity are | |
| # surfaced as warnings but do not fail the job. One of: INFO, LOW, | |
| # MEDIUM, HIGH, CRITICAL. Tune via PR. | |
| SKILL_SCANNER_BLOCK_SEVERITY: HIGH | |
| SKILL_NAME: ${{ steps.meta.outputs.skill_name }} | |
| SOURCE_DIR: ${{ steps.skill-src.outputs.source_dir }} | |
| CONFIG_FILE: ${{ matrix.config }} | |
| SCANNER_VERSION: ${{ steps.install-deps.outputs.scanner_version }} | |
| run: | | |
| scan_output="skill-scan-${SKILL_NAME}.json" | |
| scan_stderr="skill-scan-${SKILL_NAME}.stderr" | |
| python3 scripts/skill-scan/run_scan.py \ | |
| --source "$SOURCE_DIR" \ | |
| --output "$scan_output" \ | |
| 2> "$scan_stderr" || true | |
| if [ -s "$scan_stderr" ]; then | |
| echo "Scanner stderr output:" | |
| cat "$scan_stderr" | |
| fi | |
| # process_scan_results.py exits 1 on unallowlisted findings — that fails the job. | |
| python3 scripts/skill-scan/process_scan_results.py \ | |
| "$scan_output" \ | |
| "$SKILL_NAME" \ | |
| "$CONFIG_FILE" \ | |
| > scan-summary.json | |
| echo "$SCANNER_VERSION" > scanner-version.txt | |
| - name: Upload scan results | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: skill-scan-${{ steps.meta.outputs.skill_name }} | |
| path: | | |
| skill-scan-${{ steps.meta.outputs.skill_name }}.json | |
| scan-summary.json | |
| scanner-version.txt | |
| retention-days: 30 | |
| build-skill-artifacts: | |
| needs: [discover-skill-configs, validate-skills, skill-security-scan] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| # Aggregate scan result is intentionally not gated here. A failure in one | |
| # skill's scan must not block publishing the rest. The per-cell | |
| # `Pre-flight scan gate` step below preserves the security gate at the | |
| # individual skill level by reading scan-summary.json from the artifact | |
| # produced by skill-security-scan (uploaded with `if: always()`). | |
| if: ${{ !cancelled() && needs.discover-skill-configs.outputs.changed-configs != '[]' && needs.validate-skills.result == 'success' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-skill-configs.outputs.changed-configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # Needed for OIDC token (cosign keyless signing, attestations) | |
| attestations: write # Needed for actions/attest-* to publish attestations | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| - name: Install Cosign | |
| if: github.event_name != 'pull_request' | |
| uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 | |
| - name: Install yq | |
| uses: mikefarah/yq@751d8ad57b84f1794661bc70c0afb92a22ad7b3c # v4.53.2 | |
| - name: Log in to Container Registry | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata from config | |
| id: meta | |
| env: | |
| CONFIG_FILE: ${{ matrix.config }} | |
| run: | | |
| echo "config_file=$CONFIG_FILE" >> $GITHUB_OUTPUT | |
| skill_name=$(echo "$CONFIG_FILE" | cut -d'/' -f2) | |
| echo "skill_name=$skill_name" >> $GITHUB_OUTPUT | |
| version=$(yq '.spec.version' "$CONFIG_FILE" 2>/dev/null || echo "latest") | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| ref=$(yq '.spec.ref' "$CONFIG_FILE" 2>/dev/null || echo "") | |
| echo "ref=$ref" >> $GITHUB_OUTPUT | |
| repository=$(yq '.spec.repository' "$CONFIG_FILE" 2>/dev/null || echo "") | |
| echo "repository=$repository" >> $GITHUB_OUTPUT | |
| skill_path=$(yq '.spec.path // ""' "$CONFIG_FILE" 2>/dev/null || echo "") | |
| echo "skill_path=$skill_path" >> $GITHUB_OUTPUT | |
| image_name="${REGISTRY}/${{ github.repository }}/skills/${skill_name}" | |
| echo "image_name=$image_name" >> $GITHUB_OUTPUT | |
| # Pull the per-skill security scan artifact early, before we spend time | |
| # building. `continue-on-error: true` so this step succeeds when the | |
| # artifact does not exist (i.e. this skill was not in scan-configs for | |
| # this run); the next step decides whether that is acceptable. | |
| - name: Download skill security scan results | |
| id: download-scan | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: skill-scan-${{ steps.meta.outputs.skill_name }} | |
| path: /tmp/skill-scan-results | |
| continue-on-error: true | |
| # Per-cell scan gate. Replaces the old aggregate | |
| # `needs.skill-security-scan.result == 'success'` check at the job | |
| # level so that one skill's scan failure cannot starve out the rest. | |
| # - If this skill was in scan-configs for this run, require a fresh | |
| # scan-summary.json with status in {passed, warning}. Anything else | |
| # fails the cell, preserving the security gate per skill. | |
| # - If this skill was NOT in scan-configs (e.g. go.mod-only push that | |
| # rebuilds everything but only re-scans changed specs), proceed on | |
| # prior trust: the previous run on main must have been clean for the | |
| # pinned ref to land here. | |
| - name: Pre-flight scan gate | |
| env: | |
| CONFIG: ${{ matrix.config }} | |
| SCAN_CONFIGS: ${{ needs.discover-skill-configs.outputs.scan-configs }} | |
| SKILL_NAME: ${{ steps.meta.outputs.skill_name }} | |
| run: | | |
| set -euo pipefail | |
| summary=/tmp/skill-scan-results/scan-summary.json | |
| expected=$(jq -r --arg c "$CONFIG" 'index($c) // "missing"' <<<"$SCAN_CONFIGS") | |
| if [ ! -f "$summary" ]; then | |
| if [ "$expected" = "missing" ]; then | |
| echo "No scan ran for ${SKILL_NAME} in this workflow (not in scan-configs); proceeding on prior trust." | |
| exit 0 | |
| fi | |
| echo "::error title=Scan artifact missing::scan-summary.json absent for ${SKILL_NAME} but the skill was in scan-configs; refusing to publish." | |
| exit 1 | |
| fi | |
| status=$(jq -r '.status // "missing"' "$summary") | |
| case "$status" in | |
| passed|warning) | |
| echo "Skill scan status for ${SKILL_NAME}: ${status} - publish allowed." | |
| ;; | |
| *) | |
| echo "::error title=Scan blocked publish::Skill ${SKILL_NAME} scan status is '${status}'; refusing to publish." | |
| jq -r '.blocking_issues[]? | " - [\(.code)] (\(.severity)) \(.message)"' "$summary" 2>/dev/null || true | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Build dockhand | |
| run: go build -o /tmp/dockhand ./cmd/dockhand | |
| - name: Build skill artifact | |
| id: build | |
| env: | |
| CONFIG_FILE: ${{ steps.meta.outputs.config_file }} | |
| IMAGE_REF: ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }} | |
| PUSH: ${{ github.event_name != 'pull_request' }} | |
| run: | | |
| # pipefail so a dockhand failure propagates through `tee` and fails | |
| # the step. Without it, `set -e` alone would not fail on a non-zero | |
| # left-hand side of the pipeline. | |
| set -o pipefail | |
| echo "Building skill artifact for $CONFIG_FILE" | |
| build_args="--config $CONFIG_FILE --tag $IMAGE_REF" | |
| if [ "$PUSH" = "true" ]; then | |
| build_args="$build_args --push" | |
| fi | |
| log_file=$(mktemp) | |
| # Retry dockhand on non-zero exit. Matrix jobs concurrently pushing | |
| # brand-new GHCR packages regularly hit transient 5xx from the | |
| # registry (observed "500 unknown: unknown error" on manifest PUT). | |
| # A linear backoff is enough — a successful retry typically lands | |
| # within the first extra attempt. | |
| max_attempts=3 | |
| attempt=1 | |
| while :; do | |
| echo "=== dockhand build-skill attempt ${attempt}/${max_attempts} ===" | |
| if /tmp/dockhand build-skill $build_args 2>&1 | tee "$log_file"; then | |
| break | |
| fi | |
| if [ "$attempt" -ge "$max_attempts" ]; then | |
| echo "dockhand build-skill failed after ${attempt} attempt(s)" >&2 | |
| exit 1 | |
| fi | |
| backoff=$(( 10 * attempt )) | |
| echo "dockhand build-skill failed; retrying in ${backoff}s..." >&2 | |
| sleep "$backoff" | |
| attempt=$(( attempt + 1 )) | |
| done | |
| # Extract digest from captured output. `|| true` so an absent | |
| # "Digest:" line yields digest="" without failing the step; downstream | |
| # steps already gate on `steps.build.outputs.digest != ''`. | |
| digest=$(grep "^Digest:" "$log_file" | awk '{print $2}' || true) | |
| echo "digest=$digest" >> $GITHUB_OUTPUT | |
| rm -f "$log_file" | |
| - name: Sign skill artifact with Cosign | |
| if: github.event_name != 'pull_request' | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| IMAGE_NAME: ${{ steps.meta.outputs.image_name }} | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| if [ -z "$DIGEST" ]; then | |
| echo "No digest available, skipping signing" | |
| exit 0 | |
| fi | |
| echo "Signing skill artifact ${IMAGE_NAME}@${DIGEST}" | |
| cosign sign --yes "${IMAGE_NAME}@${DIGEST}" | |
| cosign sign --yes "${IMAGE_NAME}:${VERSION}" | |
| echo "Signed: ${IMAGE_NAME}@${DIGEST} and ${IMAGE_NAME}:${VERSION}" >> $GITHUB_STEP_SUMMARY | |
| - name: Check out skill source for SBOM | |
| if: github.event_name != 'pull_request' && steps.build.outputs.digest != '' | |
| id: skill-src | |
| env: | |
| SKILL_REPO: ${{ steps.meta.outputs.repository }} | |
| SKILL_REF: ${{ steps.meta.outputs.ref }} | |
| SKILL_PATH: ${{ steps.meta.outputs.skill_path }} | |
| run: | | |
| rm -rf /tmp/skill-src | |
| mkdir -p /tmp/skill-src | |
| git clone --filter=tree:0 --no-checkout "$SKILL_REPO" /tmp/skill-src/repo | |
| git -C /tmp/skill-src/repo checkout "$SKILL_REF" | |
| if [ -n "$SKILL_PATH" ]; then | |
| src_dir="/tmp/skill-src/repo/$SKILL_PATH" | |
| else | |
| src_dir="/tmp/skill-src/repo" | |
| fi | |
| echo "source_dir=$src_dir" >> $GITHUB_OUTPUT | |
| - name: Generate SBOM from skill source | |
| if: github.event_name != 'pull_request' && steps.build.outputs.digest != '' | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| path: ${{ steps.skill-src.outputs.source_dir }} | |
| format: spdx-json | |
| output-file: /tmp/skill-sbom.spdx.json | |
| upload-artifact: false | |
| upload-release-assets: false | |
| - name: Attest SBOM for skill artifact | |
| if: github.event_name != 'pull_request' && steps.build.outputs.digest != '' | |
| uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 | |
| with: | |
| subject-name: ${{ steps.meta.outputs.image_name }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| sbom-path: /tmp/skill-sbom.spdx.json | |
| push-to-registry: true | |
| - name: Attest build provenance for skill artifact | |
| if: github.event_name != 'pull_request' && steps.build.outputs.digest != '' | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ${{ steps.meta.outputs.image_name }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| - name: Create security scan attestation (SCAI format) | |
| if: github.event_name != 'pull_request' && steps.build.outputs.digest != '' | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| IMAGE_NAME: ${{ steps.meta.outputs.image_name }} | |
| CONFIG_FILE: ${{ matrix.config }} | |
| COMMIT_SHA: ${{ github.sha }} | |
| RUN_ID: ${{ github.run_id }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| # Prior-trust path: this skill was not in scan-configs for this run | |
| # (e.g. go.mod-only push that rebuilds everything but only re-scans | |
| # changed specs). The previous SCAI attestation on the registry | |
| # remains authoritative; skipping here is safe and intentional. | |
| if [ ! -f /tmp/skill-scan-results/scan-summary.json ]; then | |
| echo "No fresh scan summary for this run; skipping SCAI attestation (skill not in this run's scan-configs)." | |
| exit 0 | |
| fi | |
| SCANNER_VERSION="" | |
| if [ -f /tmp/skill-scan-results/scanner-version.txt ]; then | |
| SCANNER_VERSION=$(tr -d '\n' < /tmp/skill-scan-results/scanner-version.txt) | |
| echo "Scanner version: $SCANNER_VERSION" | |
| fi | |
| python3 scripts/skill-scan/generate_scai_attestation.py \ | |
| /tmp/skill-scan-results/scan-summary.json \ | |
| "$IMAGE_NAME" \ | |
| "$DIGEST" \ | |
| --config-file "$CONFIG_FILE" \ | |
| --commit-sha "$COMMIT_SHA" \ | |
| --run-id "$RUN_ID" \ | |
| --run-url "${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}" \ | |
| --producer-uri "${SERVER_URL}/${REPO}" \ | |
| --scanner-version "$SCANNER_VERSION" \ | |
| --scanner-uri "https://github.com/cisco-ai-defense/skill-scanner" \ | |
| --validate \ | |
| --output /tmp/skill-scai.json | |
| echo "Generated SCAI attestation:" | |
| cat /tmp/skill-scai.json | |
| cosign attest --yes \ | |
| --predicate /tmp/skill-scai.json \ | |
| --type https://in-toto.io/attestation/scai/v0.3 \ | |
| "${IMAGE_NAME}@${DIGEST}" | |
| ATTRIBUTE=$(python3 -c "import json,sys;print(json.load(open('/tmp/skill-scai.json'))['predicate']['attributes'][0]['attribute'])") | |
| echo "SCAI security scan attestation created (${ATTRIBUTE})" >> $GITHUB_STEP_SUMMARY | |
| rm -f /tmp/skill-scai.json | |
| - name: Build summary | |
| env: | |
| SKILL_NAME: ${{ steps.meta.outputs.skill_name }} | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| IMAGE_NAME: ${{ steps.meta.outputs.image_name }} | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| echo "## Skill Build: ${SKILL_NAME}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Skill | ${SKILL_NAME} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Version | ${VERSION} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | \`${IMAGE_NAME}:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY | |
| if [ -n "$DIGEST" ]; then | |
| echo "| Digest | \`${DIGEST}\` |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "| Status | ${{ github.event_name != 'pull_request' && 'Published' || 'Built (dry run)' }} |" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ github.event_name }}" != "pull_request" ] && [ -n "$DIGEST" ]; then | |
| echo "| SBOM | Attested (SPDX) |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Provenance | Attested (SLSA) |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| save-pr-number: | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Save PR number | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: echo "$PR_NUMBER" > pr-number.txt | |
| - name: Upload PR number | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: skill-pr-number | |
| path: pr-number.txt | |
| retention-days: 5 | |
| summary: | |
| needs: [discover-skill-configs, validate-skills, skill-security-scan, build-skill-artifacts] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Build summary | |
| env: | |
| CONFIGS: ${{ needs.discover-skill-configs.outputs.changed-configs }} | |
| SCAN_CONFIGS: ${{ needs.discover-skill-configs.outputs.scan-configs }} | |
| VALIDATE_RESULT: ${{ needs.validate-skills.result }} | |
| SCAN_RESULT: ${{ needs.skill-security-scan.result }} | |
| BUILD_RESULT: ${{ needs.build-skill-artifacts.result }} | |
| run: | | |
| echo "## Skill Build Pipeline Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Stage | Result |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Skills to build | $(echo "$CONFIGS" | jq '. | length') |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Skills to scan | $(echo "$SCAN_CONFIGS" | jq '. | length') |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Validation | ${VALIDATE_RESULT} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Security scan | ${SCAN_RESULT} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Build | ${BUILD_RESULT} |" >> $GITHUB_STEP_SUMMARY |