Reusable GitHub Actions workflows for Bitwise
Media Group repositories. Each consuming repo keeps a thin caller workflow that owns its triggers and calls one of
these by uses:, so CI, CodeQL, release, and the fast-forward /merge flow live in one place instead of being
copy-pasted per repo.
Copy a caller from examples/ into your repo's .github/workflows/, then pin it (see
Pinning).
All reusable workflows live in .github/workflows/ and are workflow_call-only. Every external
action is pinned to a full commit SHA; Dependabot keeps the pins fresh.
| Workflow | Platform | What it does |
|---|---|---|
ci.yaml |
any | canonical Makefile gates (lint/build/test) per job, toolchains by detection, Codecov upload |
security.yaml |
any | CodeQL over actions + go (autobuild) + javascript-typescript, language matrix by detection |
release.yaml |
any | release-please (two-pass) → GoReleaser (if .goreleaser.yaml) or dist/ rebuild + verify; optional vanity tags |
merge.yaml |
any | signature-preserving fast-forward merge — /merge now, or /auto-merge (comment/label) when approved + green |
merge-notice.yaml |
any | posts a one-time "this repo merges via /merge" comment on new PRs |
dependabot-merge.yaml |
any | auto-approves Dependabot minor/patch PRs and fast-forwards them once CI is green |
Each workflow below lists its inputs, secrets, and the permission ceiling the caller must grant — a reusable workflow's jobs cannot exceed the permissions of the job that calls them. The snippet is the minimal caller; follow the link beneath it for the fully-commented version.
Any repo. Runs the canonical Makefile gates — lint, build, test — as one parallel job each, sets up only the
toolchains the repo has (a root go.mod → Go, package.json → Node), and uploads coverage to Codecov from a job
isolated from PR-built code. An opt-in e2e job runs make e2e. A caller may add product-specific jobs (e.g.
integration) alongside the ci job.
- Inputs:
go-version-file(defaultgo.mod),node-version-file(default.node-version),cache-dependency-path(defaultgo.sum; newline-separated lockfiles to key the module cache on),e2e(defaultfalse). - Secrets: none.
- Permissions (caller grants):
contents: read.
on:
push:
branches: [main]
pull_request:
branches: ["main", "releases/*"]
permissions:
contents: read
jobs:
ci:
uses: bitwise-media-group/github-workflows/.github/workflows/ci.yaml@v2Full example: examples/ci.yaml.
Any repo. CodeQL analysis whose language matrix is detected at the repo root: actions (build-free) always, plus go
(via autobuild; setup-go matches go-version-file) when a root go.mod exists and javascript-typescript
(build-free) when package.json exists.
- Inputs:
go-version-file(defaultgo.mod),config-file(optional, default none; pass./.github/codeql/codeql-config.yaml, copyexamples/codeql-config.yaml, to exclude a bundleddist/). - Secrets: none.
- Permissions (caller grants):
security-events: write,packages: read,actions: read,contents: read.
on:
push:
branches: [main]
pull_request:
branches: ["main", "releases/*"]
schedule:
- cron: "28 20 * * 4"
permissions:
security-events: write
packages: read
actions: read
contents: read
jobs:
analyze:
uses: bitwise-media-group/github-workflows/.github/workflows/security.yaml@v2Full example: examples/security.yaml.
Any repo. Runs release-please (two-pass), then branches by detection: a repo with a .goreleaser.yaml runs GoReleaser
(archives, checksums, SBOMs, cosign signatures, optional Homebrew cask, SLSA attestation); every other repo rebuilds via
make build and verifies a committed dist/. With vanity-tags: true it also moves the floating major and minor tags
(v1 and v1.1).
- Inputs:
go-version-file(defaultgo.mod),node-version-file(default.node-version),vanity-tags(defaultfalse; move the floatingv1/v1.1tags — set it for Actions/reusable repos whose consumers pin@v1). - Secrets:
homebrew-tap-token— optional; only needed if.goreleaser.yamlpublishes a Homebrew cask to another repo (secrets.HOMEBREW_TAP_GITHUB_TOKEN). - Permissions (caller grants):
contents: write,issues: write,pull-requests: write,id-token: write,attestations: write,artifact-metadata: write. Grant all six even without a.goreleaser.yaml: GitHub resolves a reusable workflow's permissions as the union of every job and ignoresif:, so the skipped GoReleaser job'sid-token/attestations/artifact-metadataare still required or the run fails at startup.
on:
push:
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
id-token: write # cosign keyless signing
attestations: write # github build-provenance attestation
artifact-metadata: write # artifact storage record for the attestation
jobs:
release:
uses: bitwise-media-group/github-workflows/.github/workflows/release.yaml@v2
with:
vanity-tags: true # for Actions/reusable repos pinned @v1
secrets:
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} # optionalFull example: examples/release.yaml.
Any repo. Signature-preserving fast-forward merge — the manual /merge and the set-and-forget auto-merge in one
workflow. /merge merges an approved, green PR now; arming auto-merge (a /auto-merge comment or the auto-merge
label) fast-forwards it automatically the moment it is approved and every required check is green. Both run the same
ff-merge action with a short-lived App token (commit objects untouched, so signatures survive), and ff-merge
re-verifies write access, approval, all checks, and a genuine fast-forward before moving the ref. There is no GitHub
event for "all of a repo's own Actions checks finished" (GitHub does not fire check_suite for GITHUB_TOKEN runs), so
auto-merge observes completion via workflow_run(completed) listing every required workflow. Requires branch protection
that requires PR review. See org setup.
- Inputs:
app-client-id(required;vars.FF_MERGE_CLIENT_ID),merge-command(default/merge),arm-command(default/auto-merge),label(defaultauto-merge),require-approval(defaulttrue),maintainer-only(defaulttrue). - Secrets:
app-private-key— required (secrets.FF_MERGE_PRIVATE_KEY). - Permissions (caller grants): none — the App token does the privileged work, so the caller sets
permissions: {}. - Triggers (the caller owns all four):
issue_comment(created) drives/mergeand arms/auto-merge;pull_request(labeled) arms via label;pull_request_review(submitted) andworkflow_run(completed, listing your CI workflow name(s)) attempt the auto-merge once approved and green. A/merge-only repo can trigger justissue_comment. None of the triggers run PR code, and none usepull_request_target. - Fork PRs: only
issue_commentandworkflow_runcarry base-context secrets on a fork, so the label-arm and review jobs are gated to same-repo PRs. Arm a fork PR with/auto-merge(it merges via that attempt orworkflow_run), or just use/merge.
on:
issue_comment:
types: [created]
pull_request:
types: [labeled]
pull_request_review:
types: [submitted]
workflow_run:
workflows: ["CI"] # your CI workflow name(s) — all that must be green
types: [completed]
permissions: {}
jobs:
merge:
uses: bitwise-media-group/github-workflows/.github/workflows/merge.yaml@v2
with:
app-client-id: ${{ vars.FF_MERGE_CLIENT_ID }}
secrets:
app-private-key: ${{ secrets.FF_MERGE_PRIVATE_KEY }}Full example: examples/merge.yaml.
Any repo. Posts a one-time comment on newly opened PRs explaining the repo merges via /merge. Uses
pull_request_target so the notice reaches fork PRs too.
- Inputs:
pr-number— required. - Secrets: none.
- Permissions (caller grants):
pull-requests: write.
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
notice:
uses: bitwise-media-group/github-workflows/.github/workflows/merge-notice.yaml@v2
with:
pr-number: ${{ github.event.pull_request.number }}Full example: examples/merge-notice.yaml.
Any repo. Auto-approves Dependabot minor and patch PRs, then fast-forwards them into the base branch once CI is
green — using the same signature-preserving ff-merge action as merge.yaml. Major updates are never
approved, so they wait for a human. Both the approval and the merge are done with the "FF Merge" App token, so approval
works regardless of the org "Allow GitHub Actions to approve pull requests" setting (that only restricts
GITHUB_TOKEN). Requires a
.github/dependabot.yaml so
Dependabot opens PRs, and branch protection that requires PR review (so the approval sets the review decision) — the
same assumption as the /merge flow.
- Inputs:
app-client-id(required;vars.FF_MERGE_CLIENT_ID),update-types(optional; JSON array of Dependabot update types, default["version-update:semver-patch", "version-update:semver-minor"]). - Secrets:
app-private-key— required (secrets.FF_MERGE_PRIVATE_KEY). - Permissions (caller grants): none — the App token does the privileged work, so the caller job sets
permissions: {}. - Triggers (the caller owns both):
pull_request_target(opened/reopened/synchronize) to approve on open, andworkflow_run(completed) listing your CI workflow name(s) to fast-forward once they finish green.check_suiteis not used — GitHub does not fire it for a repo's own Actions CI.
on:
pull_request_target:
types: [opened, reopened, synchronize]
workflow_run:
workflows: ["CI"] # your CI workflow name(s) — all that must be green
types: [completed]
permissions: {}
jobs:
auto-merge:
uses: bitwise-media-group/github-workflows/.github/workflows/dependabot-merge.yaml@v2
with:
app-client-id: ${{ vars.FF_MERGE_CLIENT_ID }}
secrets:
app-private-key: ${{ secrets.FF_MERGE_PRIVATE_KEY }}Full example: examples/dependabot-merge.yaml.
The reusable workflows stay free of per-repo configuration by assuming a small contract. The Makefile is the language
boundary — every repo provides the same canonical targets and the workflows just run make <target>, setting up
toolchains from the files at the repo root:
- Makefile targets —
lint(all check-mode static analysis:prettier --check, markdownlint, and for Gogo vet/govulncheck),build,test(emittingcoverage/cobertura-coverage.xml, optionallycoverage/junit.xml), ande2e. Stub any that don't apply as a no-op (build: ; @:) somake <target>always succeeds; coverage is optional. - Toolchain detection —
setup-goruns only when a rootgo.modexists;setup-node(andnpm ci) only whenpackage.jsonexists. A tools-onlygo.work+tools/go.mod(forgo tool addlicense) is universal dev tooling, so it is not a Go-product signal — only a rootgo.modis. - CodeQL — scans
actionsalways,go(autobuild,setup-gofromgo-version-file) when a rootgo.modexists, andjavascript-typescriptwhenpackage.jsonexists. A repo with a bundleddist/should passconfig-file: ./.github/codeql/codeql-config.yaml(copyexamples/codeql-config.yaml) to exclude it. - Release —
release-please-config.json+.release-please-manifest.json; a.goreleaser.yaml(release-type: go,draft: true) selects the GoReleaser path, otherwise the publish path rebuilds viamake buildand verifies a committeddist/. Setvanity-tags: trueto move the floatingv1/v1.1tags.
A caller may mix a reusable-workflow job with normal jobs — e.g. a Go CLI keeps its product-specific integration /
e2e jobs in the same ci.yaml that calls the reusable ci.yaml.
The examples reference @v2, the floating major tag, which moves to each release in the v2.x line (a 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 can bump either. Avoid @main except for short-lived testing.
merge.yaml (the /merge + auto-merge flows) and dependabot-merge.yaml drive the bitwise-media-group/ff-merge
action; merge-notice.yaml posts the companion convention reminder. The one-time org setup (the "FF Merge" GitHub App,
its ruleset bypass, and the FF_MERGE_CLIENT_ID variable + FF_MERGE_PRIVATE_KEY secret) is documented in
bitwise-media-group/ff-merge.
Note on App input names. This library's contract is input
app-client-id+ secretapp-private-key, backed byvars.FF_MERGE_CLIENT_ID/secrets.FF_MERGE_PRIVATE_KEY. Existing callers across the org currently use inconsistent names (client-id/app-key, orapp-id/app-keywithFF_APP_ID/FF_APP_KEY); align them to the names above when migrating to these reusable workflows.
This repo dogfoods its own reusable workflows by local path: self-ci.yaml calls ci.yaml (which detects node only —
there is no root go.mod — and runs the canonical make gates) and self-release.yaml calls release.yaml (the
publish path, moving the vanity tags). self-security.yaml stays a bespoke actions-only scan: the library has no
compilable Go and no JS/TS product source, so the reusable security.yaml would add an empty javascript-typescript
leg from its tooling-only package.json. The /merge + auto-merge flows (self-merge.yaml), the merge notice
(self-merge-notice.yaml), and Dependabot auto-merge (self-dependabot-merge.yaml, which keeps the reusable workflows'
action pins fresh) dogfood the rest. Validate a change to a reusable workflow by temporarily pointing a real consumer's
caller at a feature branch or SHA (@your-branch) and opening a PR there.
self-release.yaml calls the reusable release.yaml (release-type: simple, vanity-tags: true) on pushes to main;
merging the release PR cuts vX.Y.Z and moves the floating major and minor tags (v2, v2.1). Consumers pin to those
tags.