diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9261e2e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# Keeps the hash-pinned action refs current. Every `uses:` in this repo is pinned +# to a commit (see .github/zizmor.yml), which is only maintainable if something +# proposes the bumps — that is this file's whole job. Without it the pins rot. +version: 2 +updates: + # For the github-actions ecosystem, `/` means ".github/workflows" plus an + # action.yml in the repo root — it is not a normal directory path. This repo has + # no composite actions, so one entry covers it. `examples/` is not scanned, but + # the only ref in there is a reusable workflow at `@main`, which is deliberately + # unpinned (see .github/zizmor.yml). + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + # Wait a week before proposing a release. Auto-bumping a commit hash means + # trusting whatever was published; most compromised or yanked releases are + # found within days, so the delay costs nothing and skips that window. + cooldown: + default-days: 7 + groups: + # One PR for all action bumps rather than one per action. + actions: + patterns: ["*"] diff --git a/.github/workflows/claude-docs-drift.yml b/.github/workflows/claude-docs-drift.yml index 9b786ac..c5d253c 100644 --- a/.github/workflows/claude-docs-drift.yml +++ b/.github/workflows/claude-docs-drift.yml @@ -138,7 +138,7 @@ jobs: - name: Generate relay token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} diff --git a/.github/workflows/claude-pr-triage.yml b/.github/workflows/claude-pr-triage.yml index e9c0fdb..e0e3d43 100644 --- a/.github/workflows/claude-pr-triage.yml +++ b/.github/workflows/claude-pr-triage.yml @@ -34,7 +34,8 @@ name: Triage PR with Claude # triage_instructions: | # -# secrets: inherit +# secrets: +# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # # Notes: # * `triage_instructions` is the per-repo rubric: what each category means for @@ -48,8 +49,8 @@ name: Triage PR with Claude # `gh label list`. It can't write files or post comments, and the workflow # never runs the PR's code (checkout only). Only two secrets are ever # referenced — ANTHROPIC_API_KEY and the permissions-scoped GITHUB_TOKEN — -# and GitHub injects a secret into a step only where it's explicitly used, so -# `secrets: inherit` doesn't widen the blast radius. +# and callers pass the first one explicitly (see the usage block above), so +# nothing else in the caller's secret store is reachable from here. # * Fork PRs are skipped (they don't get secrets). Maintainers can still run a # fork PR through `workflow_dispatch` after a sanity look. @@ -117,7 +118,7 @@ jobs: group: claude-triage-${{ github.repository }}-${{ inputs.pr_number != '' && inputs.pr_number || github.event.pull_request.number }} cancel-in-progress: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 1 persist-credentials: false @@ -160,7 +161,7 @@ jobs: - name: Triage PR id: triage - uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa # v1.0.111 + uses: anthropics/claude-code-action@d40ddef4c030e508327d6e35a9c45f3368482c50 # v1.0.195 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # Use the runner-injected GITHUB_TOKEN instead of letting the action mint diff --git a/.github/workflows/clickhouse-benchmark.yml b/.github/workflows/clickhouse-benchmark.yml index ba05eb1..dc1bbf1 100644 --- a/.github/workflows/clickhouse-benchmark.yml +++ b/.github/workflows/clickhouse-benchmark.yml @@ -190,12 +190,18 @@ jobs: - name: Mint token for the benchmarks repo id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} owner: ${{ steps.bench.outputs.owner }} repositories: ${{ steps.bench.outputs.name }} + # Without these the token carries every permission the app holds on the + # benchmarks repo. All it does is dispatch one workflow there: + # `actions: write` to dispatch, `contents: read` to resolve the ref. + permission-actions: write + permission-contents: read + - name: Trigger the StressHouse dispatch workflow env: GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/cross-repo-bug-relay.yml b/.github/workflows/cross-repo-bug-relay.yml index 9864580..64ee1c6 100644 --- a/.github/workflows/cross-repo-bug-relay.yml +++ b/.github/workflows/cross-repo-bug-relay.yml @@ -110,12 +110,15 @@ jobs: - name: Generate token for central repo id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} owner: ${{ steps.parse.outputs.owner }} repositories: ${{ steps.parse.outputs.name }} + # Without this the token carries every permission the app holds on the + # central repo. The only call it makes is one `gh issue create`. + permission-issues: write - name: Relay run: | diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..194b064 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,75 @@ +name: zizmor + +# Static analysis of this repo's GitHub Actions definitions. +# +# Everything here is a reusable workflow that other repos call, so a mistake in +# one of these files becomes a mistake in every caller. These workflows also run +# an AI agent over untrusted PR content with an app token, which puts them in the +# trust boundary rather than in build glue. zizmor catches the classes that +# matter: expression interpolation into `run:` blocks, over-broad app tokens and +# `permissions:` grants, and unreviewed action refs. +# +# The gate fails on ANY finding zizmor reports. Accepted risks are listed in +# .github/zizmor.yml with a reason, so an exception is a reviewable diff rather +# than a warning nobody reads. If this job fails, either fix the workflow or add +# the exception there. + +on: + # Deliberately unfiltered on pull requests, unlike the push trigger below. A + # run that `paths` skips reports no status at all, so this job could never be a + # required check: every pull request that left the workflows alone would wait + # for a status that never arrives. The audit takes about ten seconds, so it is + # cheaper to run it every time than to keep a filter and a skip shim in sync. + pull_request: + push: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" + # zizmor audits the Dependabot config too (cooldown windows, ecosystems), + # so a change to it has to re-run the gate. + - ".github/dependabot.yml" + - "examples/**" + workflow_dispatch: + +permissions: {} + +jobs: + zizmor: + name: Audit Actions setup + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + + - name: Run zizmor + # `pipx` is preinstalled on the GitHub-hosted Ubuntu images, so there is + # no setup action to trust here. + # + # The version is pinned: with a fail-on-anything gate, an unpinned + # zizmor would let a new release add audits that break unrelated PRs. + # Bump it deliberately, and fix or record whatever the new version finds + # in the same PR. + # + # The examples are matched as files, not as a directory: zizmor collects + # directories by convention and only looks in `.github`. They are audited + # because client repos copy them, so a permissions or interpolation + # mistake there propagates. The glob keeps new examples covered without + # anyone remembering to add them. + # + # Default `regular` persona. `--persona=pedantic` also reports style + # findings (missing concurrency limits, uncommented `permissions:`) that + # are not security properties and would make the gate noisy. + # + # GH_TOKEN enables the online audits, which check action refs against + # GitHub advisories and confirm the refs still exist. Those need the API, + # and they are the reason a workflow-touching PR can start failing for a + # newly published advisory — that is the intent. + run: pipx run zizmor==1.29.0 --no-progress .github/ examples/*.yml + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..5ab44a6 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,24 @@ +# zizmor configuration — https://docs.zizmor.sh/configuration/ +# +# The CI gate (.github/workflows/zizmor.yml) fails on ANY finding zizmor still +# reports. So this file is the complete list of accepted risks in this repo's +# Actions setup, and each entry must say why. Fix the workflow first; add an +# entry here only when the finding is wrong or the risk is understood and taken. +# +# There is no `unpinned-uses` policy block on purpose: zizmor's default blanket +# policy already requires a full commit hash, and every `uses:` in this repo is +# pinned that way. .github/dependabot.yml proposes the bumps weekly so the pins +# stay current — the two files are a pair, and dropping either one leaves you +# with refs that are stale or unpinned. + +rules: + unpinned-uses: + ignore: + # Callers are told to reference these reusable workflows at `@main` — that + # is the point of publishing them here, and pinning callers to a commit + # would mean every client repo needs a bump for every fix. The example + # callers are audited anyway so that a `run:` block or permissions mistake + # in one cannot be copied into seven repos unnoticed. Add a line here when + # you add an example caller. + - caller-claude-pr-triage.yml + - caller-claude-docs-drift.yml diff --git a/examples/caller-claude-pr-triage.yml b/examples/caller-claude-pr-triage.yml index 298ebee..4bf84a4 100644 --- a/examples/caller-claude-pr-triage.yml +++ b/examples/caller-claude-pr-triage.yml @@ -6,7 +6,7 @@ # `.claude/skills/triage-pr/SKILL.md`). # # Requirements in the caller repo: -# * Secret `ANTHROPIC_API_KEY`, reachable via `secrets: inherit`. +# * Secret `ANTHROPIC_API_KEY`, passed through explicitly (see the bottom). # * Labels using the `triage:*` and `risk:*` prefixes will be created/applied. name: Triage PR with Claude @@ -23,6 +23,15 @@ on: jobs: triage: + # Required, not optional. A called workflow can only narrow the permissions + # its caller grants, never widen them — so if you leave this out, the job + # runs on your repo's default token and triage fails to apply labels or post + # its comment whenever that default is read-only. These three are exactly + # what claude-pr-triage.yml declares; do not add to them. + permissions: + contents: read + pull-requests: write # labels + the sticky results comment + issues: read # lets Claude read an issue the PR links to uses: ClickHouse/integrations-shared-workflows/.github/workflows/claude-pr-triage.yml@main with: # pull_request runs read the PR from the event; only workflow_dispatch @@ -74,4 +83,8 @@ jobs: - low — AI review only; eligible for auto-merge per repo policy. - medium — at least one human reviewer. - high — PR body must include an architectural description before review. - secrets: inherit + # Passed explicitly rather than with `secrets: inherit`. This is the only + # secret the reusable workflow declares, so naming it keeps the rest of this + # repo's secret store out of reach of a workflow in another repository. + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}