chore: bump version to 0.4.1 #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
| --- | |
| name: Release | |
| # Attested delivery for this plugin/marketplace repo: reproducible tarball, | |
| # SLSA build provenance, CycloneDX SBOM, six quality-gate verdicts | |
| # (SAST/SCA/Trivy/Semgrep/Secrets/Manifest) each seam-attested as a signed | |
| # custom predicate bound to the tarball's digest, an OpenVEX vulnerability | |
| # disposition, and a cosign-signed marketplace.json catalog — all fail-closed | |
| # verified before the tag-gated publish step. | |
| # | |
| # Unlike a multi-repo org, this repo has no separate central signer: every | |
| # attestation and the catalog signature are produced by THIS workflow running | |
| # in THIS repo. Verification still pins the signer workflow explicitly | |
| # (`--signer-workflow zircote/cdc-error-plugin/.github/workflows/release.yml` | |
| # for gh attestation verify; the cosign certificate-identity-regexp is | |
| # anchored to this same workflow path) — `--repo` alone only proves "some | |
| # workflow in this repo signed it," not specifically this one. See | |
| # SECURITY.md and docs/how-to/verify-release.md. | |
| # | |
| # Flow: build tarball -> attest-build-provenance | |
| # -> [gate-* -> attest-*] x6 (parallel), sbom (generate + attest), | |
| # sign-catalog (cosign), vex (normalize + attest) | |
| # -> verify (fail-closed) -> publish (tag push only). | |
| # | |
| # Dry-run: workflow_dispatch exercises the full chain through verify; publish | |
| # only runs on an actual tag push. | |
| "on": | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| meta: | |
| name: Resolve Metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| name: ${{ steps.meta.outputs.name }} | |
| version: ${{ steps.meta.outputs.version }} | |
| steps: | |
| - name: Resolve artifact name and version | |
| id: meta | |
| run: | | |
| set -euo pipefail | |
| NAME="${GITHUB_REPOSITORY##*/}" | |
| if [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| else | |
| VERSION="0.0.0-dev" | |
| fi | |
| echo "name=${NAME}" >> "$GITHUB_OUTPUT" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved: ${NAME} ${VERSION}" | |
| build: | |
| name: Build Artifact | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| outputs: | |
| subject-name: ${{ steps.subject.outputs.name }} | |
| subject-digest: ${{ steps.subject.outputs.digest }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Build reproducible tarball | |
| env: | |
| NAME: ${{ needs.meta.outputs.name }} | |
| VERSION: ${{ needs.meta.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| git archive --format=tar.gz --prefix="${NAME}-${VERSION}/" \ | |
| -o "dist/${NAME}-${VERSION}.tar.gz" "${GITHUB_SHA}" | |
| sha256sum dist/* | |
| - name: Record subject name + digest (for gate attestations) | |
| id: subject | |
| env: | |
| NAME: ${{ needs.meta.outputs.name }} | |
| VERSION: ${{ needs.meta.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| FILE="${NAME}-${VERSION}.tar.gz" | |
| DIGEST=$(sha256sum "dist/${FILE}" | cut -d' ' -f1) | |
| echo "name=${FILE}" >> "$GITHUB_OUTPUT" | |
| echo "digest=sha256:${DIGEST}" >> "$GITHUB_OUTPUT" | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-path: dist/* | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-dist | |
| path: dist/* | |
| if-no-files-found: error | |
| # --- quality gates: each produces a SARIF/JSON evidence file ----------- | |
| gate-sast: | |
| name: gate-sast | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| languages: actions | |
| build-mode: none | |
| - name: Perform CodeQL analysis | |
| uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| category: /language:actions | |
| output: codeql-sarif | |
| - name: Normalize SARIF filename | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| files=(codeql-sarif/*.sarif) | |
| if [ "${#files[@]}" -eq 0 ]; then echo "::error::CodeQL produced no SARIF"; exit 1; fi | |
| jq -s '{"$schema": .[0]."$schema", "version": .[0].version, "runs": (map(.runs) | add)}' \ | |
| "${files[@]}" > results.sarif | |
| # No separate "upload to code scanning" step here: codeql-action/analyze | |
| # already uploads to the Security tab itself (default upload: always). | |
| # A second upload-sarif call with the same category fails with | |
| # "only one run of the codeql/analyze or codeql/upload-sarif actions is | |
| # allowed per job per tool/category" — this is the one gate whose tool | |
| # self-uploads; every other gate below needs an explicit upload step | |
| # because its scanner doesn't. | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sast-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| gate-sca: | |
| name: gate-sca | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Run OSV-Scanner | |
| uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 | |
| continue-on-error: true | |
| with: | |
| scan-args: |- | |
| --output=results.json | |
| --format=json | |
| --recursive | |
| ./ | |
| - name: Generate SARIF report | |
| uses: google/osv-scanner-action/osv-reporter-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 | |
| with: | |
| scan-args: |- | |
| --output=results.sarif | |
| --new=results.json | |
| --gh-annotations=false | |
| --fail-on-vuln=true | |
| - name: Upload to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: results.sarif | |
| category: osv-scanner | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sca-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| gate-trivy: | |
| name: gate-trivy | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # SECURITY: aquasecurity/trivy-action is pinned to a full commit SHA. | |
| # In March 2026 (CVE-2026-33634) an attacker force-pushed 76 of 77 | |
| # trivy-action tags to credential-stealing malware. Never replace this | |
| # SHA with a tag. | |
| - name: Trivy IaC + license scan | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| scan-type: fs | |
| scan-ref: . | |
| scanners: misconfig,license | |
| format: sarif | |
| output: results.sarif | |
| severity: HIGH,CRITICAL | |
| exit-code: '0' | |
| - name: Upload to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: results.sarif | |
| category: trivy-iac-license | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: trivy-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| gate-semgrep: | |
| name: gate-semgrep | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install Semgrep (pinned) | |
| run: | | |
| set -euo pipefail | |
| python3 -m venv "${RUNNER_TEMP}/semgrep-venv" | |
| "${RUNNER_TEMP}/semgrep-venv/bin/pip" install --no-cache-dir --disable-pip-version-check \ | |
| "semgrep==1.168.0" | |
| echo "${RUNNER_TEMP}/semgrep-venv/bin" >> "$GITHUB_PATH" | |
| - name: Semgrep -> SARIF | |
| run: | | |
| set -euo pipefail | |
| semgrep scan --config p/security-audit --config p/secrets --config p/command-injection \ | |
| --sarif --output results.sarif \ | |
| --metrics off --disable-version-check --no-error . || true | |
| if [ ! -f results.sarif ]; then echo "::error::Semgrep produced no SARIF"; exit 1; fi | |
| - name: Upload to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: results.sarif | |
| category: semgrep | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: semgrep-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| gate-secrets: | |
| name: gate-secrets | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install Gitleaks + TruffleHog (checksum-verified) | |
| run: | | |
| set -euo pipefail | |
| dest="${RUNNER_TEMP}/secrets-bin"; mkdir -p "${dest}" | |
| dl() { curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 -o "$1" "$2"; } | |
| GL_VER=8.30.1 | |
| glbase="https://github.com/gitleaks/gitleaks/releases/download/v${GL_VER}" | |
| glzip="gitleaks_${GL_VER}_linux_x64.tar.gz" | |
| dl "${dest}/${glzip}" "${glbase}/${glzip}" | |
| dl "${dest}/gl.sums" "${glbase}/gitleaks_${GL_VER}_checksums.txt" | |
| ( cd "${dest}" && grep " ${glzip}\$" gl.sums | sha256sum --check --strict - ) | |
| tar -xzf "${dest}/${glzip}" -C "${dest}" gitleaks | |
| TH_VER=3.95.8 | |
| thbase="https://github.com/trufflesecurity/trufflehog/releases/download/v${TH_VER}" | |
| thzip="trufflehog_${TH_VER}_linux_amd64.tar.gz" | |
| dl "${dest}/${thzip}" "${thbase}/${thzip}" | |
| dl "${dest}/th.sums" "${thbase}/trufflehog_${TH_VER}_checksums.txt" | |
| ( cd "${dest}" && grep " ${thzip}\$" th.sums | sha256sum --check --strict - ) | |
| tar -xzf "${dest}/${thzip}" -C "${dest}" trufflehog | |
| chmod +x "${dest}/gitleaks" "${dest}/trufflehog" | |
| echo "${dest}" >> "$GITHUB_PATH" | |
| - name: Gitleaks -> SARIF | |
| run: | | |
| set -euo pipefail | |
| gitleaks dir . --report-format sarif --report-path results.sarif --exit-code 0 --no-banner | |
| if [ ! -f results.sarif ]; then | |
| echo "::error::Gitleaks produced no SARIF (--exit-code 0 always exits 0 on findings, so a missing file means the scan itself failed to run, not that it found nothing)" | |
| exit 1 | |
| fi | |
| - name: TruffleHog (verified-only, hard-fail) | |
| run: | | |
| set -euo pipefail | |
| trufflehog filesystem . --results=verified --json --no-update > trufflehog.json | |
| n="$(grep -c . trufflehog.json || true)" | |
| echo "TruffleHog verified findings: ${n:-0}" | |
| if [ "${n:-0}" -gt 0 ]; then | |
| echo "::error::TruffleHog confirmed ${n} VERIFIED live secret(s), failing closed." | |
| exit 1 | |
| fi | |
| - name: Upload to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: results.sarif | |
| category: gitleaks | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: secrets-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| gate-manifest: | |
| name: gate-manifest | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Review manifests -> SARIF | |
| run: | | |
| set -euo pipefail | |
| python3 - . > results.sarif <<'PY' | |
| import json, os, re, sys | |
| root = sys.argv[1] | |
| SHA = re.compile(r'^[0-9a-f]{40}$') | |
| RESERVED = { | |
| "claude-code-marketplace", "claude-code-plugins", "claude-plugins-official", | |
| "claude-plugins-community", "claude-community", "anthropic-marketplace", | |
| "anthropic-plugins", "agent-skills", "anthropic-agent-skills", | |
| "knowledge-work-plugins", "life-sciences", "claude-for-legal", | |
| "claude-for-financial-services", "financial-services-plugins", | |
| } | |
| results = [] | |
| def add(rule, level, text, path, line=1): | |
| results.append({"ruleId": rule, "level": level, | |
| "message": {"text": text}, | |
| "locations": [{"physicalLocation": { | |
| "artifactLocation": {"uri": path}, | |
| "region": {"startLine": line}}}]}) | |
| def rel(p): | |
| return os.path.relpath(p, root).replace(os.sep, "/") | |
| for dirpath, _dirs, files in os.walk(root): | |
| if ".git" in dirpath.split(os.sep): | |
| continue | |
| for fn in files: | |
| full = os.path.join(dirpath, fn) | |
| if fn == "marketplace.json" and ".claude-plugin" in dirpath: | |
| try: | |
| with open(full, encoding="utf-8") as fh: | |
| mp = json.load(fh) | |
| except Exception as e: | |
| add("MR001", "error", "marketplace.json is not valid JSON: %s" % e, rel(full)); continue | |
| if not mp.get("name"): | |
| add("MR002", "error", "marketplace.json missing required 'name'", rel(full)) | |
| elif mp["name"] in RESERVED: | |
| add("MR003", "error", "marketplace name '%s' is Anthropic-reserved" % mp["name"], rel(full)) | |
| if not (mp.get("owner") or {}).get("name"): | |
| add("MR004", "error", "marketplace.json missing required 'owner.name'", rel(full)) | |
| for i, p in enumerate(mp.get("plugins", [])): | |
| src = p.get("source") | |
| name = p.get("name", "plugins[%d]" % i) | |
| if not name or not src: | |
| add("MR005", "error", "plugin entry '%s' missing required name/source" % name, rel(full)) | |
| if isinstance(src, dict) and src.get("source") in ("github", "url", "git-subdir"): | |
| sha = src.get("sha", "") | |
| if not SHA.match(str(sha)): | |
| add("MR006", "error", | |
| "external plugin '%s' source is not pinned to a 40-char sha (got %r)" % (name, sha), | |
| rel(full)) | |
| elif fn == "plugin.json" and ".claude-plugin" in dirpath: | |
| try: | |
| with open(full, encoding="utf-8") as fh: | |
| pl = json.load(fh) | |
| except Exception as e: | |
| add("MR007", "error", "plugin.json is not valid JSON: %s" % e, rel(full)); continue | |
| for req in ("name", "description"): | |
| if not pl.get(req): | |
| add("MR008", "error", "plugin.json missing required '%s'" % req, rel(full)) | |
| if not (pl.get("author") or {}).get("name"): | |
| add("MR009", "error", "plugin.json missing required 'author.name'", rel(full)) | |
| sarif = {"version": "2.1.0", | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| "runs": [{"tool": {"driver": {"name": "manifest-review", | |
| "rules": [{"id": r} for r in sorted({x["ruleId"] for x in results})]}}, | |
| "results": results}]} | |
| print(json.dumps(sarif)) | |
| PY | |
| - name: Upload to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: results.sarif | |
| category: manifest-review | |
| - name: Upload SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: manifest-sarif | |
| path: results.sarif | |
| if-no-files-found: error | |
| # --- seam: bind each gate's evidence to the tarball digest as a signed | |
| # custom-predicate attestation. This repo is both the artifact repo AND | |
| # the signer (no separate central org), so verification only needs | |
| # --repo, not --signer-workflow. --------------------------------------- | |
| attest-sast: | |
| name: attest-sast | |
| needs: [build, gate-sast] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sast-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/sast/v1 | |
| predicate-path: predicate/results.sarif | |
| attest-sca: | |
| name: attest-sca | |
| needs: [build, gate-sca] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sca-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/sca/v1 | |
| predicate-path: predicate/results.sarif | |
| attest-trivy: | |
| name: attest-trivy | |
| needs: [build, gate-trivy] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: trivy-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/iac-license/v1 | |
| predicate-path: predicate/results.sarif | |
| attest-semgrep: | |
| name: attest-semgrep | |
| needs: [build, gate-semgrep] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: semgrep-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/semgrep/v1 | |
| predicate-path: predicate/results.sarif | |
| attest-secrets: | |
| name: attest-secrets | |
| needs: [build, gate-secrets] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: secrets-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/secrets/v1 | |
| predicate-path: predicate/results.sarif | |
| attest-manifest: | |
| name: attest-manifest | |
| needs: [build, gate-manifest] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: manifest-sarif | |
| path: predicate | |
| - name: Attest | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://zircote.github.io/attestations/manifest/v1 | |
| predicate-path: predicate/results.sarif | |
| # --- SBOM ------------------------------------------------------------- | |
| sbom: | |
| name: SBOM (generate + attest) | |
| needs: [meta, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Download build artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-dist | |
| path: dist | |
| - name: Generate CycloneDX SBOM | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| path: . | |
| format: cyclonedx-json | |
| output-file: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-sbom.cdx.json | |
| upload-artifact: false | |
| upload-release-assets: false | |
| - name: Attest SBOM | |
| uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 | |
| with: | |
| subject-path: dist/* | |
| sbom-path: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-sbom.cdx.json | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-sbom | |
| path: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-sbom.cdx.json | |
| if-no-files-found: error | |
| # --- cosign keyless-sign the marketplace.json catalog blob ------------- | |
| sign-catalog: | |
| name: sign-catalog | |
| needs: [meta] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| outputs: | |
| bundle: ${{ steps.sign.outputs.bundle }} | |
| identity: ${{ steps.sign.outputs.identity }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Sign blob (keyless) + verify back (fail-closed) | |
| id: sign | |
| env: | |
| # This repo's own workflow is the signer (no separate central seam), | |
| # so the Fulcio cert SAN is this release.yml. Deliberately NOT | |
| # anchored to a specific @ref (e.g. refs/tags/*): this job has no | |
| # tag-only `if:` gate, so it also runs (and self-verifies) on a | |
| # workflow_dispatch dry-run from a branch. Anchoring to | |
| # refs/tags/ here would make the dry-run's own in-run verify-blob | |
| # check fail every time. The residual risk (a signature from a | |
| # non-tag ref passing this same regex) requires push access to | |
| # this repo to exploit, which is already the trust boundary for a | |
| # solo-maintained repo with no other collaborators. | |
| SIGNER_IDENTITY_RE: '^https://github\.com/zircote/cdc-error-plugin/\.github/workflows/release\.yml@' | |
| run: | | |
| set -euo pipefail | |
| BLOB=.claude-plugin/marketplace.json | |
| bundle="marketplace.json.cosign.bundle" | |
| cosign sign-blob --yes "${BLOB}" --bundle "${bundle}" | |
| cosign verify-blob "${BLOB}" \ | |
| --bundle "${bundle}" \ | |
| --certificate-identity-regexp "${SIGNER_IDENTITY_RE}" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" | |
| echo "bundle=${bundle}" >> "$GITHUB_OUTPUT" | |
| echo "identity=${SIGNER_IDENTITY_RE}" >> "$GITHUB_OUTPUT" | |
| - name: Upload cosign bundle | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cosign-bundle | |
| path: ${{ steps.sign.outputs.bundle }} | |
| if-no-files-found: error | |
| # --- OpenVEX disposition ---------------------------------------------- | |
| vex: | |
| name: vex | |
| needs: [meta, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: 'stable' | |
| - name: Install vexctl | |
| run: go install "github.com/openvex/vexctl@v0.4.4" | |
| - name: Normalize OpenVEX document | |
| run: | | |
| set -euo pipefail | |
| vexctl merge .vex/openvex.json > openvex.normalized.json | |
| jq -e . openvex.normalized.json > /dev/null | |
| - name: Attest OpenVEX document | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-name: ${{ needs.build.outputs.subject-name }} | |
| subject-digest: ${{ needs.build.outputs.subject-digest }} | |
| predicate-type: https://openvex.dev/ns/v0.2.0 | |
| predicate-path: openvex.normalized.json | |
| # --- fail-closed verification (MUST pass before publish) ---------------- | |
| verify: | |
| name: Verify Attestations (fail-closed) | |
| needs: [meta, build, sbom, attest-sast, attest-sca, attest-trivy, | |
| attest-semgrep, attest-secrets, attest-manifest, sign-catalog, vex] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| attestations: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Download build artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-dist | |
| path: dist | |
| - name: Download cosign bundle | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: cosign-bundle | |
| path: catalog-sig | |
| - name: Fail-closed attestation + catalog-signature verification | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| # Pin the signer workflow, not just the repo: --repo alone accepts | |
| # an attestation from ANY workflow in this repo with | |
| # id-token:write + attestations:write, not specifically this one. | |
| SIGNER: ${{ github.repository }}/.github/workflows/release.yml | |
| SUBJECT_NAME: ${{ needs.build.outputs.subject-name }} | |
| CATALOG_SIGNER_ID: ${{ needs.sign-catalog.outputs.identity }} | |
| run: | | |
| set -euo pipefail | |
| artifact="dist/${SUBJECT_NAME}" | |
| test -f "${artifact}" || { echo "::error::expected artifact not found: ${artifact}"; exit 1; } | |
| echo "=== Verifying: ${artifact} ===" | |
| gh attestation verify "${artifact}" --repo "${REPO}" --signer-workflow "${SIGNER}" \ | |
| --predicate-type https://slsa.dev/provenance/v1 | |
| gh attestation verify "${artifact}" --repo "${REPO}" --signer-workflow "${SIGNER}" \ | |
| --predicate-type https://cyclonedx.org/bom | |
| for pt in sast sca iac-license semgrep secrets manifest; do | |
| gh attestation verify "${artifact}" --repo "${REPO}" --signer-workflow "${SIGNER}" \ | |
| --predicate-type "https://zircote.github.io/attestations/${pt}/v1" | |
| echo " PASS: ${pt}" | |
| done | |
| gh attestation verify "${artifact}" --repo "${REPO}" --signer-workflow "${SIGNER}" \ | |
| --predicate-type https://openvex.dev/ns/v0.2.0 | |
| echo " PASS: VEX" | |
| echo "=== Verifying catalog signature (cosign) ===" | |
| cosign verify-blob .claude-plugin/marketplace.json \ | |
| --bundle catalog-sig/marketplace.json.cosign.bundle \ | |
| --certificate-identity-regexp "${CATALOG_SIGNER_ID}" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" | |
| echo "All attestations + catalog signature verified." | |
| publish: | |
| name: Publish Release | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') | |
| needs: [meta, verify, sign-catalog] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download build artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-dist | |
| path: dist | |
| - name: Download SBOM artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.meta.outputs.name }}-${{ needs.meta.outputs.version }}-sbom | |
| path: dist | |
| - name: Download cosign bundle | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: cosign-bundle | |
| path: dist | |
| - name: Generate checksums | |
| env: | |
| NAME: ${{ needs.meta.outputs.name }} | |
| VERSION: ${{ needs.meta.outputs.version }} | |
| run: | | |
| cd dist | |
| sha256sum -- * > "${NAME}-${VERSION}-checksums.txt" | |
| cat "${NAME}-${VERSION}-checksums.txt" | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.meta.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| PRERELEASE=() | |
| case "${GITHUB_REF_NAME}" in *-*) PRERELEASE=(--prerelease) ;; esac | |
| gh release create "${GITHUB_REF_NAME}" dist/* \ | |
| --repo "${GITHUB_REPOSITORY}" --title "Release ${VERSION}" \ | |
| --generate-notes "${PRERELEASE[@]}" |