diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..829eef1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + # This repo ships skills, hook scripts, and JSON manifests with no application + # package manager, so the only ecosystem to keep current is the pinned GitHub + # Actions in .github/workflows. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/actions-lint.yml b/.github/workflows/actions-lint.yml new file mode 100644 index 0000000..ac42bbb --- /dev/null +++ b/.github/workflows/actions-lint.yml @@ -0,0 +1,34 @@ +name: Actions Security + +on: + push: + branches: [main] + paths: [".github/workflows/**"] + pull_request: + branches: [main] + paths: [".github/workflows/**"] + +permissions: + contents: read + +concurrency: + group: actions-lint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + name: zizmor (workflow audit) + runs-on: ubuntu-latest + steps: + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Run zizmor + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pipx install zizmor + zizmor --min-severity=medium .github/workflows/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd82279..3a7ee6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,12 +9,19 @@ on: permissions: contents: read +# Cancel superseded runs on the same ref so rapid pushes don't pile up runners. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: validate: name: Validate plugin structure runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Validate JSON files run: | @@ -28,6 +35,24 @@ jobs: fi done + # Belt-and-suspenders: parse every committed JSON file (manifests + + # fixtures), not just the three core manifests above. node is preinstalled + # on ubuntu-latest; node_modules/.git are excluded. + - name: Validate all JSON parses + run: | + set -euo pipefail + fail=0 + while IFS= read -r -d '' f; do + if node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$f"; then + echo " [ok] $f" + else + echo " [FAIL] invalid JSON: $f" + fail=1 + fi + done < <(find . -name '*.json' \ + -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/dist/*' -print0) + exit $fail + - name: Validate plugin version consistency run: | echo "── Version consistency ──" @@ -87,21 +112,41 @@ jobs: exit $FAIL shellcheck: - name: Shellcheck hooks + name: Shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Run shellcheck on hook scripts + # shellcheck is preinstalled on ubuntu-latest. Scope to error severity so + # the gate catches real bugs without failing on stylistic notes. Covers + # every committed *.sh (hooks, libs, installers, tests), not just hooks/. + - name: Run shellcheck on all shell scripts run: | - echo "── Shellcheck ──" - FAIL=0 - for f in hooks/*.sh test-local.sh; do - if shellcheck --severity=error "$f"; then - echo " [ok] $f" - else - echo " [FAIL] $f" - FAIL=1 - fi - done - exit $FAIL + set -euo pipefail + shellcheck --version + mapfile -d '' files < <(find . -name '*.sh' \ + -not -path './.git/*' -not -path '*/node_modules/*' -print0) + if [ ${#files[@]} -eq 0 ]; then + echo "no shell scripts found"; exit 0 + fi + printf 'checking: %s\n' "${files[@]}" + shellcheck --severity=error "${files[@]}" + + # ── Aggregate gate ─────────────────────────────────────────────────────── + # One stable required-status-check context. Fails if any upstream job failed + # OR was skipped/cancelled, so the job graph can be reshaped without orphaning + # the required check. + ci-success: + name: CI Passed + if: always() + needs: [validate, shellcheck] + runs-on: ubuntu-latest + steps: + - name: Verify all required jobs succeeded + run: | + echo "validate=${{ needs.validate.result }}" + echo "shellcheck=${{ needs.shellcheck.result }}" + [ "${{ needs.validate.result }}" = "success" ] \ + && [ "${{ needs.shellcheck.result }}" = "success" ] diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..0e50d69 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,38 @@ +name: Secret Scan + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: secrets-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + steps: + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 # scan full history + persist-credentials: false + # The gitleaks GitHub Action requires a paid license for org accounts, but + # the gitleaks binary itself is free (MIT). Run it directly. + - name: Install gitleaks + env: + GITLEAKS_VERSION: "8.30.1" + run: | + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + - name: Scan repository + run: gitleaks dir . --redact --verbose --exit-code 1 diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..a919abd --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,4 @@ +# zizmor configuration. All findings are fixed in-place (least-privilege +# permissions + persist-credentials: false on every checkout), so there are +# currently no accepted exceptions to record here. +rules: {}