diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6f1775c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + # This repo ships markdown + JSON manifests + shell install scripts and has 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 new file mode 100644 index 0000000..0eac399 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +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: + # ── Config / manifest validation ───────────────────────────────────────── + # This repo has no build system: it's markdown + JSON manifests + shell + # install scripts. The gate lints the shell and parses every JSON file. + validate: + name: Validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + # shellcheck is preinstalled on ubuntu-latest runners. Scope to error + # severity: this gate catches real bugs (unset vars, bad quoting that + # changes behavior) without failing on stylistic SC2086-class notes. + - name: Shellcheck (severity=error) + run: | + 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[@]}" + + # Validate that every committed JSON manifest parses. node is preinstalled + # on ubuntu-latest. node_modules/dist/.git are excluded. + - name: Validate JSON + 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 + + # ── Aggregate gate ─────────────────────────────────────────────────────── + # One stable required-status-check context. Fails if validate 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] + runs-on: ubuntu-latest + steps: + - name: Verify all required jobs succeeded + run: | + echo "validate=${{ needs.validate.result }}" + [ "${{ needs.validate.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: {}