actions-reusable-workflows · diff
git:20260701.6f09c20 to git:20260816.3aca11f
159 added, 45 removed. Audit A to A.
---
name: actions-reusable-workflows
- description: Wire a repository to the bitwise-media-group/github-workflows reusable workflows — thin caller workflows for CI, CodeQL, release (release-please/GoReleaser), and the signature-preserving fast-forward /merge, merge-notice, and Dependabot auto-merge flows — plus the Makefile consumer contract, @v2 pinning, and FF Merge org setup. Use when setting up CI, CodeQL, releases, or auto-merge for a repo, adding a caller that uses the shared/reusable workflows, scaffolding a repo's .github/ with the org's standard automation, or asked which reusable workflow to call and what to grant it.
+ description: Wire a repository to the bitwise-media-group/github-workflows reusable workflows — thin, SHA-pinned caller workflows for CI, security (CodeQL) scanning, release (release-please/GoReleaser/Zensical docs), the signature-preserving fast-forward /merge + merge-review-ack + merge-notice flows, and add-to-project — plus the bitwise-media-group/toolchain mise-task consumer contract and the FF Merge / Project Sync org app setup. Use when setting up CI, security scanning, releases, auto-merge, dependency updates, or project-board sync for a repo; adding a caller that uses the shared/reusable workflows; wiring the mise toolchain library; scaffolding a repo's .github/ with the org's standard automation; or asked which reusable workflow to call and what to grant it.
license: MIT
---
# Wire the bitwise reusable workflows
- The org centralises CI, CodeQL, release, and the fast-forward `/merge` automation in the
- `bitwise-media-group/github-workflows` library. A consuming repo keeps a **thin caller** in
- `.github/workflows/` that owns its triggers and grants a permission ceiling, then delegates the work
- to a reusable workflow with `uses:`. Copy a caller from this skill's [templates/](templates/)
- directory and pin it.
+ The org centralises CI, security scanning, release, and the fast-forward `/merge` automation in the
+ `bitwise-media-group/github-workflows` library, and its mise task contract in
+ `bitwise-media-group/toolchain`. A consuming repo keeps a **thin caller** in `.github/workflows/`
+ that owns its triggers and grants a permission ceiling, then delegates the work to a reusable
+ workflow with `uses:`. Copy a caller from this skill's [templates/](templates/) directory and pin it
+ by SHA.
Author bespoke workflows with the `actions-style` skill, harden them with `actions-security`, and
run `actions-validate` before committing.
## 1. The caller pattern
The caller declares `on:`, sets the `permissions:` ceiling the reusable workflow's jobs need (a
reusable workflow can never exceed the caller's grant), and calls one workflow per job:
```yaml
name: Continuous integration
on:
push:
branches: [main]
pull_request:
branches: ["main", "releases/*"]
permissions:
contents: read
jobs:
ci:
- uses: bitwise-media-group/github-workflows/.github/workflows/ci.yaml@v2
+ uses: bitwise-media-group/github-workflows/.github/workflows/ci.yaml@1eaff010e2929a3583c50ffa325690216bc70462 # v6.2.0
```
A caller may add product-specific jobs (an `integration` job, say) alongside the reusable-workflow
job. None of these workflows use `pull_request_target` to run PR code.
## 2. Catalog
- Each workflow has a ready-to-copy template; grant exactly the listed ceiling and nothing more. None
- of these run PR code or use `pull_request_target` unsafely.
+ Every reusable workflow lives in the same repo and releases together — one tag, one commit, covers
+ `ci.yaml`, `security.yaml`, `release.yaml`, every merge workflow, and `add-to-project.yaml` at once.
+ Grant exactly the listed permission ceiling and nothing more.
- - **`ci.yaml`** ([template](templates/ci.yaml)) — canonical Makefile gates (lint/build/test) plus
- Codecov upload. Caller grants `contents: read`. Inputs: `e2e` (default `false`), `go-version-file`,
- `node-version-file`, `cache-dependency-path`. No secrets.
- - **`codeql.yaml`** ([template](templates/codeql.yaml)) — CodeQL over `actions` + `go` +
+ ### Always wire these
+
+ - **`ci.yaml`** ([template](templates/ci.yaml)) — runs the canonical mise tasks (`lint`, `build`,
+ `test`, optional `e2e`) as parallel jobs, sets up only the toolchains the repo has (`go.mod` → Go,
+ `package.json` → Node, `pyproject.toml` → Python via `uv`), verifies a committed `dist/` stays
+ reproducible, and uploads coverage to Codecov. Caller grants `contents: read`. Inputs:
+ `go-version-file`, `node-version-file`, `cache-dependency-path`, `e2e` (default `false`),
+ `coverage` (default `true`; set `false` for repos that emit neither coverage nor test results).
+ No secrets.
+ - **`security.yaml`** ([template](templates/security.yaml)) — CodeQL over `actions` + `go` +
`javascript-typescript`, by detection. Caller grants `security-events: write`, `packages: read`,
`actions: read`, `contents: read`. Inputs: `go-version-file`, `config-file` (pass
`./.github/codeql/codeql-config.yaml`, [template](templates/codeql-config.yaml), to exclude a
- bundled `dist/`). No secrets.
- - **`release.yaml`** ([template](templates/release.yaml)) — release-please → GoReleaser (when a
- `.goreleaser.yaml` exists) or a `dist/` rebuild + verify. Caller grants
- `contents`/`issues`/`pull-requests: write`, plus `id-token`/`attestations`/`artifact-metadata: write`
- for the GoReleaser path only. Inputs: `vanity-tags` (default `false`; set it for Actions/reusable
- repos pinned `@v1`). Optional secret `homebrew-tap-token`.
+ bundled `dist/`), `languages` (override auto-detection — e.g. a tooling-only `package.json` that
+ shouldn't add an empty `javascript-typescript` leg). No secrets.
+ - **`release.yaml`** ([template](templates/release.yaml)) — release-please (two-pass) → GoReleaser
+ (when a `.goreleaser.yaml` exists) and/or a Zensical docs build published to GitHub Pages (when a
+ `zensical.toml` exists) — independent of each other, so one release can ship both. Caller grants
+ `contents`/`issues`/`pull-requests: write`, plus `id-token`/`attestations`/`artifact-metadata`/
+ `packages`/`pages: write` — grant all seven even without a `.goreleaser.yaml` or `zensical.toml`:
+ GitHub unions a reusable workflow's job permissions and ignores `if:`, so a missing scope fails the
+ run at startup regardless of which jobs actually run. Inputs: `vanity-tags` (default `false`; set
+ it for Actions/reusable repos pinned `@v1`), `app-client-id` (optional — author the release PR as
+ the "FF Merge" App instead of `GITHUB_TOKEN`, needed only if you auto-merge release PRs via
+ `merge.yaml`, since a `GITHUB_TOKEN` push emits no `workflow_run` event). Optional secrets
+ `homebrew-tap-token`, `app-private-key` (required only when `app-client-id` is set). Outputs:
+ `release_created`, `tag_name`, `sha`, `version`, `major`, `minor`, `patch` — gate a follow-on job
+ with `if: needs.release.outputs.release_created` (using the caller's own job id).
- **`merge.yaml`** ([template](templates/merge.yaml)) — signature-preserving fast-forward `/merge`
plus set-and-forget auto-merge. Caller grants `permissions: {}` (the App token does the privileged
- work). Inputs: `app-client-id` (required), `require-approval`/`maintainer-only` (default `true`),
- `label`, `merge-command`, `arm-command`. Required secret `app-private-key`.
+ work). Inputs: `app-client-id` (required), `merge-command`/`arm-command`/`label` (default
+ `/merge`/`/auto-merge`/`auto-merge`), `require-approval`/`maintainer-only` (default `true`),
+ `squash-authors` (default `bitwise-renovate[bot]` — authors whose PRs are squash-merged via the API
+ instead of fast-forwarded, since their branches never rebase onto the base). Required secret
+ `app-private-key`. Triggers `issue_comment`, `workflow_run` (list your CI workflow name(s) plus
+ `"Merge Review Ack"`), and a `schedule` backstop sweep — deliberately no PR-attached event, so the
+ flow adds no skipped-check clutter to a PR.
+ - **`merge-review-ack.yaml`** ([template](templates/merge-review-ack.yaml)) — required companion to
+ `merge.yaml`: carries the **approval** signal (`pull_request_review`, which `merge.yaml`
+ deliberately doesn't subscribe to) back in as a `workflow_run`, so fork and same-repo PRs auto-merge
+ identically the moment they're approved. Caller grants `permissions: {}`, no inputs, no secrets —
+ add `"Merge Review Ack"` to `merge.yaml`'s `workflow_run.workflows` list. Its single `ack` job is
+ the only check this whole system adds to a PR.
- **`merge-notice.yaml`** ([template](templates/merge-notice.yaml)) — posts a one-time "this repo
merges via `/merge`" comment on new PRs. Caller grants `pull-requests: write`. Input: `pr-number`
(required). No secrets.
- - **`dependabot-merge.yaml`** ([template](templates/dependabot-merge.yaml)) — auto-approves
- Dependabot minor/patch PRs and fast-forwards them once CI is green. Caller grants `permissions: {}`.
- Inputs: `app-client-id` (required), `update-types`. Required secret `app-private-key`.
+ - **`add-to-project.yaml`** ([template](templates/add-to-project.yaml)) — adds newly opened issues to
+ a shared org Projects v2 board via a "Project Sync" App token (`GITHUB_TOKEN` cannot write an
+ org-level project). Caller grants `permissions: {}`. Inputs: `project-url` (required;
+ `https://github.com/orgs/<org>/projects/<n>`), `app-client-id` (required),
+ `labeled`/`label-operator` (optional label filter, default operator `OR`). Required secret
+ `app-private-key`.
- The merge flows also need a [`dependabot.yaml`](templates/dependabot.yaml) and the org setup in §5.
+ ## 3. The consumer contract: mise tasks, not raw Makefile targets
- ## 3. The consumer contract
+ The reusable workflows stay config-free by assuming a small contract, and as of the v3+ line
+ **the mise config is the language boundary, not the Makefile** — `ci.yaml` runs `mise run <task>`
+ directly, so a Makefile isn't required at all.
- The reusable workflows stay config-free by assuming a small contract — the **Makefile is the
- language boundary**, and toolchains are set up from marker files at the repo root:
+ - **mise tasks** — `lint`, `build`, `test` (emitting `coverage/cobertura-coverage.xml`, optionally
+ `coverage/junit.xml`), and `e2e`, defined in a root `mise.toml` or supplied by the shared task
+ library (below). `ci.yaml` discovers the task list with `mise tasks ls --name-only` and skips
+ whichever of those four the repo hasn't defined — **no no-op stubs needed** (drop any leftover
+ `build: ; @:` from the old Makefile-only contract).
+ - **Toolchain detection** — a root `go.mod` sets up Go (`setup-go`, from `go-version-file`); a root
+ `package.json` sets up Node (`setup-node`); a root `pyproject.toml` sets up Python via
+ `astral-sh/setup-uv` (uv reads `requires-python` / `.python-version` itself — no `setup-python`). A
+ tools-only `go.work` + `tools/go.mod` is dev tooling, not a product signal, so it does **not**
+ trigger the Go path — only a root `go.mod` does.
+ - **Release** — `release-please-config.json` + `.release-please-manifest.json`; an optional
+ `.goreleaser.yaml` (`release-type: go`, `draft: true`) selects the GoReleaser path; an optional
+ `zensical.toml` (+ `pyproject.toml` + `uv.lock`, with repo **Settings → Pages → Source** set to
+ **GitHub Actions**) selects the docs-publish path independently of GoReleaser. A committed `dist/`
+ is verified fresh by `ci.yaml` on every PR, not at release time.
- - **Makefile targets** `lint`, `build`, `test` (emitting `coverage/cobertura-coverage.xml`), and
- `e2e`. Stub any that don't apply as a no-op so `make <target>` always succeeds:
+ ### Wire the toolchain library
- ```makefile
- build: ## no-op: nothing to build
- @:
- ```
+ A repo gets the mise tasks above from `bitwise-media-group/toolchain`
+ ([latest `v2.4.1`](https://github.com/bitwise-media-group/toolchain/releases/tag/v2.4.1)), mounted as
+ a submodule at `.mise/`:
- - **Toolchain detection** — a root `go.mod` selects Go (`setup-go`); a `package.json` selects Node
- (`setup-node` + `npm ci`). A tools-only `go.work` + `tools/go.mod` is dev tooling, not a Go
- product, so it does **not** trigger the Go path.
- - **Release** — `release-please-config.json` + `.release-please-manifest.json`; an optional
- `.goreleaser.yaml` (`release-type: go`, `draft: true`) selects the GoReleaser path, otherwise the
- publish path rebuilds via `make build` and verifies a committed `dist/`.
+ ```sh
+ git submodule add https://github.com/bitwise-media-group/toolchain.git .mise
+ git -C .mise checkout v2.4.1
+ git add .mise && git commit -m "chore: add bitwise-media-group/toolchain submodule"
+ ```
+ Then pick an archetype in a root `mise.toml` and reduce the `Makefile` to one include, kept only for
+ `make <target>` muscle memory and local interchangeability — CI never invokes it:
+
+ ```toml
+ # mise.toml
+ [task_config]
+ includes = [".mise/tasks/go-cli.toml"] # or node-action / node-lib / docs-site / markdown-lib / terraform
+ ```
+
+ ```makefile
+ # Makefile — the whole thing
+ include .mise/mise.mk
+ ```
+
+ Run `mise trust --all` once per clone (CI trusts the workspace automatically). The library's
+ `config.toml` also supplies the pinned developer tools, `[settings]`, and the universal
+ license/prose/container/shell lint tasks every archetype shares — see the library's own `README.md`
+ for the full task/knob reference, and the `terraform-validate` skill for the `terraform.toml`
+ archetype specifically. Per that same README, the submodule itself bumps via Dependabot's
+ `gitsubmodule` ecosystem; the tool pins inside `config.toml` bump via Renovate as normal (§7) —
+ **never** run `mise lock`/`mise upgrade` inside a consumer repo, that writes into the submodule's
+ working tree.
+
+ A caller may still mix a reusable-workflow job with normal jobs — e.g. a Go CLI keeps a
+ product-specific `integration` job in the same `ci.yaml` that calls the reusable `ci.yaml`.
+
## 4. Pinning
- The templates pin `@v2`, the floating major tag that moves to each release in the v2.x line (the
- matching minor tag `@v2.1` moves too). Pin to a release tag (`@v2.1.0`) or a full commit SHA for
- stricter supply-chain guarantees; Dependabot's `github-actions` ecosystem can bump either. Avoid
- `@main` except for short-lived testing against a feature branch.
+ Every `uses:` line in this skill's templates pins the same full commit SHA, with the release tag as
+ a trailing comment:
+ ```yaml
+ uses: bitwise-media-group/github-workflows/.github/workflows/ci.yaml@1eaff010e2929a3583c50ffa325690216bc70462 # v6.2.0
+ ```
+
+ A floating tag (`@v6`, `@v6.2`) is simpler to read but mutable — see the `actions-security` skill for
+ why that matters on a workflow with elevated permissions. Prefer the SHA; Renovate's
+ `github-actions` manager bumps either form, and for a SHA pin it updates the trailing comment too.
+ The upstream repo's own illustrative snippets are an unreliable guide to "latest" — they show
+ inconsistent floating placeholders (`@v2` on most workflows, `@v4` on `add-to-project.yaml`, `@v6` on
+ `update-tools.yaml`) reflecting whatever major existed when each example was last touched, not the
+ current release. Resolve the real latest before pinning — and when the SHA above goes stale, the same
+ way: `git ls-remote --tags https://github.com/bitwise-media-group/github-workflows | sort -t/ -k3 -V`
+ for the newest tag, then `git rev-parse <tag>` against a local clone for its commit. Avoid `@main`
+ except for short-lived testing against a feature branch.
+
## 5. Fast-forward merge: org setup
- `merge.yaml`, `dependabot-merge.yaml`, and `merge-notice.yaml` drive the
+ `merge.yaml`, `merge-review-ack.yaml`, and `merge-notice.yaml` drive the
`bitwise-media-group/ff-merge` action via a short-lived GitHub App token (so commit signatures
survive). The one-time, org-wide setup — the "FF Merge" App, its ruleset bypass, the
`FF_MERGE_CLIENT_ID` variable, and the `FF_MERGE_PRIVATE_KEY` secret — is documented in
[`bitwise-media-group/ff-merge`](https://github.com/bitwise-media-group/ff-merge). The contract is
input `app-client-id` (from `vars.FF_MERGE_CLIENT_ID`) + secret `app-private-key` (from
`secrets.FF_MERGE_PRIVATE_KEY`); align older callers using `client-id`/`app-key` to these names. The
merge flows also require branch protection that requires PR review.
+
+ ## 6. Add to Project: org setup
+
+ `add-to-project.yaml` needs its own GitHub App — "Project Sync" — separate from "FF Merge": the merge
+ flows act within a repo, while writing to an org-level Projects v2 board needs organization-scoped
+ permissions no repo-level App grant covers. Create it with organization **projects** read/write plus
+ repository **issues** + **pull requests** read, install it on every repository, and expose it as the
+ `ADD_TO_PROJECT_CLIENT_ID` variable + `ADD_TO_PROJECT_PRIVATE_KEY` secret. In this org the caller
+ itself is fanned out to every repo automatically by `org-config.sh workflows-sync` in
+ [`github-settings`](https://github.com/bitwise-media-group/github-settings), so copying the
+ [template](templates/add-to-project.yaml) by hand is only needed for a repo outside that sync.
+
+ ## 7. Dependency updates: Renovate, not Dependabot
+
+ The org replaced Dependabot with a self-hosted Renovate bot
+ ([`bitwise-media-group/renovate-config`](https://github.com/bitwise-media-group/renovate-config)).
+ It runs hourly with an App-installation token (so its commits are API-created and GitHub-signed —
+ Verified), autodiscovers every repo the "Renovate" App is installed on, and every discovered repo
+ inherits the org preset with **no onboarding file** — do not add a `.github/dependabot.yaml` or a
+ `dependabot-merge.yaml` caller. Renovate squash-merges its own green, approved PRs via the API
+ (the `merge.yaml` caller's `squash-authors` input defaults to `bitwise-renovate[bot]`), so getting a
+ repo covered is exactly: get the App installed (an org-level change, not a per-repo one).
+
+ A repo only needs its own `.github/renovate.json5` for a deliberate override on top of the preset
+ (an ignored dependency, a `postUpgradeTasks` rebuild rule for a committed bundle, …):
+
+ ```json5
+ {
+ extends: ["github>bitwise-media-group/renovate-config:default.json5"],
+ // overrides here
+ }
+ ```