From ceb64d06295ccb655aeb1b62199215ff7b92813b Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:22:45 -0400 Subject: [PATCH] feat(workflows): add reusable _dispatch-flake-consumers workflow Centralises the flake-consumer dispatch sender so every upstream repo that needs to notify downstream consumers on a release (or flake.lock push) can use a ~9-line caller instead of duplicating the full 41-line token-mint + matrix-dispatch job. Mirrors the conventions of the sibling _update-flake-input.yml: - @v3 create-github-app-token, vars.GH_APP_CLIENT_ID / GH_APP_PRIVATE_KEY - env-var injection guards on all run: steps - permissions: {} at top level, contents:read at job level - uses ${GITHUB_REPOSITORY_OWNER} instead of a hardcoded org name vars.DISPATCH_CONSUMERS and vars.GH_APP_CLIENT_ID resolve in the caller's context (workflow_call inherits caller vars), so they are read directly inside this workflow without input plumbing. GH_APP_PRIVATE_KEY is forwarded via `secrets: inherit` in each caller. The source_input default (${{ github.event.repository.name }}) resolves to the caller repo name, so callers typically need no `with:` block. Unblocks: dryvist/nix-claude-code#44, dryvist/nix-ai#869 Assisted-by: Claude:claude-opus-4-8 --- .../workflows/_dispatch-flake-consumers.yml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/_dispatch-flake-consumers.yml diff --git a/.github/workflows/_dispatch-flake-consumers.yml b/.github/workflows/_dispatch-flake-consumers.yml new file mode 100644 index 0000000..5295526 --- /dev/null +++ b/.github/workflows/_dispatch-flake-consumers.yml @@ -0,0 +1,65 @@ +# Reusable: Dispatch update-flake-input to downstream consumers +# +# Fans out a repository_dispatch (event_type=update-flake-input, +# client_payload.input=) to every repo in the caller's +# DISPATCH_CONSUMERS variable. Each caller keeps its own trigger and its +# own DISPATCH_CONSUMERS list; this workflow holds the shared token-mint +# and cross-repo dispatch call so it lives in exactly one place. +# +# vars.DISPATCH_CONSUMERS, vars.GH_APP_CLIENT_ID, and the +# GH_APP_PRIVATE_KEY secret all resolve in the caller's context +# (workflow_call inherits the caller's vars/secrets), so they are read +# here directly rather than passed as inputs. Pass GH_APP_PRIVATE_KEY +# via `secrets: inherit` in the caller. +name: _dispatch-flake-consumers + +on: + workflow_call: + inputs: + source_input: + description: >- + Flake input name the releasing repo represents downstream. + Sent as client_payload.input so the consumer knows which input + to bump. Defaults to the calling repository's own name. + type: string + required: false + default: ${{ github.event.repository.name }} + secrets: + GH_APP_PRIVATE_KEY: + required: true + +permissions: {} + +jobs: + dispatch: + name: Notify ${{ matrix.repo }} + strategy: + matrix: + repo: ${{ fromJSON(vars.DISPATCH_CONSUMERS) }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.GH_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + # Scope the minted token to just the one consumer repo. + repositories: ${{ matrix.repo }} + # repository_dispatch requires contents:write on the target. + permission-contents: write + + - name: Dispatch update-flake-input + # Use env vars — never interpolate inputs or matrix directly into + # run: steps (expression injection risk). + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + TARGET_REPO: ${{ matrix.repo }} + SOURCE_INPUT: ${{ inputs.source_input }} + run: | + gh api "repos/${GITHUB_REPOSITORY_OWNER}/${TARGET_REPO}/dispatches" \ + -f event_type=update-flake-input \ + -f "client_payload[input]=${SOURCE_INPUT}"