Shared GitHub Actions for Roastbook repositories: reusable workflows, composite actions, and the Orcastrate templates that keep a thin caller workflow in every managed repository up to date.
Managed repositories never copy CI logic. Each one has small caller files such as
.github/workflows/ci.yml that start with Orcastrate frontmatter and call a
reusable workflow from this repository at a pinned commit:
# @orcastrate
# template: open-coffee/ci
# params:
# kind: data
# @end-orcastrate
name: CI
# …
uses: Roastbook/workflows/.github/workflows/dataset-validate.yml@<sha> # mainAll live in .github/workflows/, run on ubuntu-26.04, and set minimal per-job
permissions. They call the repository's own scripts, so CI and local runs behave
the same way.
| Workflow | Runs | Inputs (default) |
|---|---|---|
schema-validate.yml |
scripts/validate.sh with BASE_REF set to the pull request base, plus actionlint |
actionlint (true), timeout_minutes (15) |
dataset-validate.yml |
as above, plus the agent path rule: pull requests opened by agent_login may only change agent_allowed_paths |
agent_login (roastbook-sourcing[bot]), agent_allowed_paths (datasets/coffee-places/**), actionlint (true), timeout_minutes (15) |
site-build.yml |
scripts/build.sh on a checkout with full history and tags, so the build can read the release version with git describe; pull requests upload a site-preview-pr-<n> artifact, other events the Pages artifact. A release tag that is no longer the head of the default branch builds the default branch instead, so a late tag deploy cannot roll the site back |
dist (dist), preview_retention_days (7), timeout_minutes (20) |
pages-deploy.yml |
actions/deploy-pages into environment github-pages, concurrency group pages |
none; output page_url |
link-check.yml |
lychee via scripts/link-check.sh (warn-only on pull requests, --full and failing otherwise) and HTTP smoke checks on scheduled and manual runs |
link_check (true), full (false), smoke_urls (""), lychee_version and its lychee_sha256, timeout_minutes (30) |
release.yml |
the release flow below | see below |
agent-run.yml |
scripts/agent.sh with a Sourcing App token scoped to the calling repository (only in GH_TOKEN; the checkout keeps no credentials), with .cache/pages cached between runs |
app_id, llm_base_url, llm_model, budget_usd (all required), timeout_minutes (180); secrets app_private_key, llm_api_key |
Every job that runs a script ends with the summary action, which turns
.ci/report.json into a "CI summary" in the job summary.
release.yml is the shared release implementation for every repository. The
caller starts it with workflow_dispatch on the default branch. It:
- requires a successful push run of
ci_workflowfor this exact commit; - computes the next version:
semverasks git-cliff for the bump from Conventional Commits (or applies the explicitbump_type);dateusesYYYY.MM.DD.Nfor today in UTC (.1, then.2the same day); - stops without releasing when
bump_typeisautoand there are no unreleased Conventional Commits, reuses an existing tag on the same commit, and refuses tags that go backwards or point elsewhere; - tags the tested commit and creates or publishes the GitHub Release with git-cliff notes, using the releaser App so tag and release events trigger other workflows (for example the Pages deploy on tags).
Optional hooks, all off by default:
| Input | Default | Effect |
|---|---|---|
bump_type |
auto |
auto, patch, minor or major |
ci_workflow |
ci.yml |
workflow file that must have passed |
version_scheme |
semver |
semver (vX.Y.Z) or date (YYYY.MM.DD.N) |
cliff_config, git_cliff_version, git_cliff_sha256 |
cliff.toml, v2.14.2, its checksum |
git-cliff configuration, and the release binary with the SHA-256 of git-cliff-<version>-x86_64-unknown-linux-gnu.tar.gz (set both together) |
node_setup |
false | run the setup action (Bun, bun install) before the assets command |
assets_command |
"" | shell command with TAG and VERSION set, e.g. scripts/release-assets.sh "$TAG" |
assets_glob |
release/* |
files to upload to the release (directories are skipped) |
npm_publish, npm_package_dirs, npm_registry, node_version |
false, release/npm, https://npm.pkg.github.com, 26 |
npm publish of each whitespace-separated directory built by assets_command, in order, with Trusted Publishing on npmjs.org or GITHUB_TOKEN on GitHub Packages; versions that already exist are skipped, so reruns are safe |
dispatch_repository, dispatch_event |
"", released |
send repository_dispatch with client_payload: {version, tag} to a repository of the same owner |
Secrets, both optional: RELEASER_APP_PRIVATE_KEY and RELEASER_APP_ID (or the
RELEASER_APP_ID variable). The common/release template passes exactly these two
secrets, not secrets: inherit. Without the private key the release falls back to the
job token, which still tags and publishes the release but loses:
- workflows triggered by the tag or release, because GitHub does not start workflows for
events created with
GITHUB_TOKEN(for example the Pages deploy on release tags); - the
repository_dispatchtodispatch_repository, which is skipped with a warning (the job token cannot write to another repository).
npm packages go to the registry in npm_registry:
- npmjs.org (
https://registry.npmjs.org): Trusted Publishing. The job gets an OIDC token (id-token: write) and publishes with provenance and public access; no npm token is stored. One-time setup per package on npmjs.com: the package belongs to theroastbooknpm organisation, and its Trusted Publisher is this caller (repositoryRoastbook/<repo>, workflowrelease.yml). npm only lets you add a Trusted Publisher to an existing package, so the very first version is published by hand (npm publish --access publicfromrelease/npmof a release). - GitHub Packages (
https://npm.pkg.github.com): the job token (packages: write). Installing needs a token even for public packages.
The caller job must grant the same permissions; the common/release template grants
id-token: write and packages: write when npm_publish is set (otherwise
packages: read for the setup action). Each package needs publishConfig.registry
set to the registry and a repository field pointing at the publishing repository.
The setup action exports NPM_GITHUB_TOKEN=${{ github.token }} to
bun install, so a consumer maps its scope in bunfig.toml:
[install.scopes]
roastbook = { url = "https://npm.pkg.github.com/", token = "$NPM_GITHUB_TOKEN" }Tools that read .npmrc instead:
@roastbook:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_GITHUB_TOKEN}GitHub Packages needs a token even for public packages. Every job that runs the
setup action therefore has packages: read, and the Open Coffee caller
templates grant it. A public package can be installed by workflows in any
repository; a private or internal one must list the consuming repository under
package settings → Manage Actions access. Locally, use
export NPM_GITHUB_TOKEN="$(gh auth token)" (the token needs read:packages:
gh auth refresh -s read:packages).
.github/actions/setup: Bun (oven-sh/setup-bun) from.bun-version, a cache of~/.bun/install/cachekeyed bybun.lock, andbun install --frozen-lockfilewithNPM_GITHUB_TOKENset for GitHub Packages. Inputs:install(true),working-directory(.),bun-version-file(.bun-version;package.jsonreadspackageManager: "bun@x.y.z")..github/actions/summary: renders.ci/report.json(records and files validated, versions, build size, URL, warnings) into$GITHUB_STEP_SUMMARY. A missing file is reported, not failed. Input:path.
Reusable workflows load these actions at a full commit SHA, so a caller's pin of
Roastbook/workflows also fixes the actions it runs. A commit cannot reference its own
SHA, so the pin is the last commit that changed .github/actions, set by
scripts/pin-actions.sh. To change an action:
- Merge the change to
.github/actions/(the workflows still use the previous pin). - On an up-to-date
main, runbash scripts/pin-actions.shand merge the result aschore: pin composite actions to <sha>. - Only then bump
templates/_workflows-ref.ymland the callers to a commit that contains step 2.
validate.yml runs scripts/pin-actions.sh --check: it fails when the references
differ or the pin is not an ancestor of HEAD, and warns while .github/actions has
changes that are not pinned yet. Keep action inputs backward compatible, because
callers on older pins still run older actions.
| Template | Caller file | Params |
|---|---|---|
open-coffee/ci |
ci.yml (CI) |
kind: schema or data |
open-coffee/build |
build.yml (Build, pull requests) |
none |
open-coffee/link-check |
link-check.yml (Link check, weekly) |
kind, site_url, smoke_paths (list), cron |
open-coffee/deploy |
deploy.yml (Deploy, main and release tags) |
kind, release_tags |
open-coffee/data-agent |
data-agent.yml (Data agent) |
cron |
open-coffee/bump-schema |
bump-schema.yml (on schema-released, rewrites the $schema URLs in datasets/**/*.json to the new version and opens "chore(schema): bump to vX" listing the changed files) |
none |
common/release |
release.yml (Release); free-form string params are rendered single-quoted, so # and : survive |
ci_workflow, version_scheme, node_setup, assets_command, assets_glob, npm_publish, npm_package_dirs, npm_registry, dispatch_repository, dispatch_event, name |
fixtures/ holds one caller per supported parameter set. The callers in
open-coffee-schema and open-coffee-data are exactly the rendered fixtures
schema-*.yml and data-*.yml.
Presets that Orcastrate does not sync (it only manages .github/workflows/):
- Renovate: every repository extends the shared base config from
michidk/workflows
with
"extends": ["github>michidk/workflows//templates/presets/renovate"]and adds only repository-specific rules (for example weeklyRoastbook/workflowsbumps). Libraries such as@roastbook/uiextendgithub>michidk/workflows//templates/presets/renovate-libraryinstead: it keeps compatible version ranges for consumers rather than pinning exact versions. templates/presets/cliff.toml: git-cliff configuration forrelease.yml. Copy it tocliff.tomlin the repository root; it accepts bothvX.Y.ZandYYYY.MM.DD.Ntags.
New callers pin Roastbook/workflows to the commit defined once in
templates/_workflows-ref.yml. Orcastrate leaves the SHA in existing callers alone,
and Renovate bumps it weekly in each managed repository. Update the template value
when new callers should start from a newer commit.
- Merge a template change into
main. sync.ymlruns Orcastrate on push, weekly, or manually. It finds every repository in the Roastbook org with the topicmanaged-workflows.- Orcastrate opens one pull request per drifted caller workflow.
This works for any repository, including michidk/* repositories that move into the Roastbook org.
-
Add the topic
managed-workflowsto the repository, or add an entry toorchestrator.toml:[[repos]] name = "Roastbook/example"
-
Install the GitHub Apps listed below on the repository.
-
Add each caller under
.github/workflows/with only the frontmatter, then runorcastrate render .github/workflows/<file>.ymland commit its output (or wait for the sync pull request). For releases:# @orcastrate # template: common/release # params: # ci_workflow: ci.yml # @end-orcastrate
-
Copy
templates/presets/cliff.tomltocliff.tomlwhen the repository releases. -
Add
.github/renovate.json5extendinggithub>michidk/workflows//templates/presets/renovate(renovate-libraryfor libraries). -
For Pages sites, add the repository to
scripts/repo-metadata.shand run it.
Create these in the Roastbook org (org-level variables and secrets, shared with the listed repositories):
- Orcastrate App (Contents, Pull requests, Workflows, Issues: read and write),
installed on this repository and every managed repository.
ORCASTRATE_APP_ID(variable) andORCASTRATE_PRIVATE_KEY(secret) forworkflows. - Releaser App (Contents: read and write, Pull requests: read and write),
installed on open-coffee-schema and open-coffee-data.
RELEASER_APP_ID(variable or secret) andRELEASER_APP_PRIVATE_KEY(secret) for both. It tags releases, sendsschema-released, and opens the schema bump pull request. - Sourcing App (Contents and Pull requests: read and write), installed on
open-coffee-data only; its GitHub App login must be
roastbook-sourcing[bot].SOURCING_APP_ID(variable) andSOURCING_APP_PRIVATE_KEY(secret). Add the rulesets in Data agent rulesets. - open-coffee-data:
LLM_API_KEY(secret);LLM_BASE_URL,LLM_MODEL,SOURCING_BUDGET_USD(variables). - After the first open-coffee-schema release: make
@roastbook/open-coffee-schemaand@roastbook/open-coffee-uipublic (package settings → Change visibility), or addopen-coffee-dataunder Manage Actions access (role Read). - Org Actions policy allows
Roastbook/workflowsand the pinned third-party actions.
The data agent reads untrusted web pages while holding a token with Contents and Pull
requests write. agent-run.yml scopes that token to the calling repository and keeps it
out of the Git configuration, but only rulesets stop the App from writing elsewhere. The
agent creates branches named data-agent/<city>-<date>-<id> and opens pull requests into
main. Recommended rulesets for open-coffee-data (Settings → Rules → Rulesets → New
branch ruleset; not applied by scripts/repo-metadata.sh):
-
Sourcing App branches
- Enforcement status: Active
- Bypass list (Always allow): Repository admin role, Maintain role, Write role, the Orcastrate App, the Releaser App and the Renovate App. Do not add the Sourcing App.
- Target branches: Include all branches; Exclude by pattern
data-agent/** - Rules: Restrict creations, Restrict updates, Restrict deletions
The Sourcing App can then create and update only
data-agent/**branches. GitHub Apps are not matched by the role entries; confirm after setup that an API branch creation outsidedata-agent/**with a Sourcing App token is rejected. -
Protect main
- Enforcement status: Active
- Bypass list: empty
- Target branches: Include default branch
- Rules: Restrict deletions; Block force pushes; Require a pull request before
merging (1 required approval, dismiss stale approvals when new commits are pushed,
require approval of the most recent reviewable push, require conversation
resolution, allowed merge method Squash); Require status checks to pass, with
"Require branches to be up to date before merging" and the checks
Validate / Validate dataset,Validate / Agent path ruleandValidate / Lint workflows.
Agent path ruleonly runs on the agent's pull requests and counts as passed when skipped; on agent pull requests it rejects changes outsideagent_allowed_paths.
scripts/repo-metadata.sh [--dry-run] [repo…] applies descriptions, homepages,
topics, Discussions on, wiki and projects off, squash merge only, branch deletion
after merge, private vulnerability reporting, and Pages (GitHub Actions source,
custom domain, HTTPS, github-pages environment accepting main and release tags).
It is idempotent; run it again after DNS resolves so HTTPS can be enforced.
See CONTRIBUTING.md and AGENTS.md for the rules and the checks that CI runs in validate.yml.