forge
Keeps a GitHub account's repositories in line with one manifest. It sets what the API can set: repo settings, Actions permissions, rulesets, variables, secrets and deployment environments. It checks and reports what needs a commit or a person to fix: shared CI workflows, Cargo lints, Dockerfiles, releases that didn't reach crates.io, Homebrew or winget, and open Dependabot alerts.
Install
Binaries for Linux, macOS and Windows are on the
releases page. forge calls the
API through the GitHub CLI, so gh must be
installed and signed in.
Getting started
forge init tags each repo by language and visibility, under a starter set
of settings and tags. Remove repos forge shouldn't manage and adjust the rest
before the first forge apply. Only keys in the manifest are checked, so
anything it leaves out stays as it is.
The manifest is the first of:
--manifest PATH, or$FORGE_MANIFEST;forge.tomlin the current directory;forge/forge.tomlin the config directory:$XDG_CONFIG_HOME, else~/.config(%APPDATA%on Windows).
A repo of its own gives the manifest history and review. Symlink it into the config directory to run forge from anywhere.
Commands
forge stale covers every repo the owner has, managed or not, leaving out
issues with a label in [stale] ignore_labels (a backlog of feature ideas,
say). It only lists them.
Manifest
= "you"
= "v"
[]
= false
[]
= ["rust-ci"]
[]
= ["rust", "security"]
= ["protect-default-branch"]
-
Top-level
settings,actionsandrulesetsapply to every repo. A repo's own table overrides them:settingsandactionskey by key,rulesetswholesale. -
[settings]goes toPATCH /repos/{owner}/{repo}, so any key that endpoint takes works. -
[actions]goes to/actions/permissions(whether Actions run and which actions may),[actions.workflow]to/actions/permissions/workflow(theGITHUB_TOKEN's defaults), and[actions.selected-actions]to the allowlist, checked whileallowed_actionsisselected. -
rulesets = [...]names rulesets, applied as repository rulesets. Private repos are skipped, since rulesets there need GitHub Pro. Built in:protect-default-branch: no deleting or force-pushing the default branchrequire-pr: changes to the default branch go through a PRrequire-pr-title: the shared PR title check must passprotect-release-tags: no deleting or movingvX.Y.Ztags
A
rulesets/NAME.jsonnext to the manifest adds a ruleset, or replaces a built-in one, with a body in the REST API shape. -
release_tag_prefix: the latest GitHub release of each repo must be tagged this plusX.Y.Z. A repo can set its own, orreleases = falsefor releases that aren't versions of it (a Homebrew tap's bottles, say).
Tags
A repo's tags = [...] bring the requirements under [tags.NAME]. A tag
without a table is just a label. A tag can carry:
settings, applied over the top-level ones and under the repo's own;rulesets, added to the repo's own;variables = { NAME = "value" }, Actions variables;secrets = { NAME = "~/path" }, Actions secrets, read from local files. They're only set when missing, since GitHub never reveals their values;forge apply --update-secretsoverwrites them, after renewing a token, say.forbidden_secrets = [...]must not exist;[tags.NAME.environments.ENV], a deployment environment that must exist.deploy_tags = [...]lets only those tag patterns deploy to it, and no branch;calls = [...], shared workflows the repo must call (see below). A repo declines one with theno-WORKFLOWtag;needs_public = true, for features GitHub only offers public repos. Every public repo then has to carry the tag or decline it withno-NAME, and a private one carrying it is reported, so a visibility change shows up;[tags.NAME.lints], lints every Cargo package must set (or inherit from[workspace.lints]), at least this strictly, in Cargo's[lints]shape;dockerfile_runs = [...], commands someRUNof the root Dockerfile must run.
Lints, Dockerfiles and workflow calls are only reported, since fixing them means committing to the repo.
Shared workflows
[workflows] maps the file a repo calls a shared workflow from to the
workflow's uses: reference:
[]
= "lkurcak/forge/.github/workflows/rust-ci.yml@v1"
forge reads each shared workflow at that ref, and checks that callers use the
same ref and only inputs it has. Calling one tags the repo with the
workflow's name (rust-ci) and with each input it turns on
(container: true tags it container), so those tags' requirements apply.
This repo's workflows are public, and anyone can call them at @v1, which
moves along with compatible changes:
rust-ci.yml: fmt, clippy, tests on Linux, macOS and Windows, docs, packaging and the MSRV. Withcontainer: trueit builds and publishes a multi-arch image to ghcr.io instead of testing on macOS and Windows.rust-release.yml: on avX.Y.Ztag, publishes to crates.io through trusted publishing, attaches binaries to the GitHub release, and bumps the formula in{owner}/homebrew-tapand the package in winget-pkgs. forge then checks the latest release reached each of them.pr-title.yml: PR titles follow Conventional Commits.t-ci.yml: checks repos written in T.
Each file's header shows how to call it, and its inputs say what they do.
Also reported
- A latest release whose tag doesn't match
release_tag_prefix, that lacks one of the archives its release workflow builds, or that hasn't reached crates.io, the Homebrew formula or winget-pkgs. An open PR delivering it shows as pending. - Open Dependabot alerts, as pending while a bot PR fixing one is under a week old. Bot PRs open longer than a week are reported too.
Credentials
forge uses the token gh holds for the manifest's owner
(gh auth token --user OWNER), so the active gh account doesn't matter.
When gh isn't signed in as the owner, as for an organization, it uses the
active account's token instead, which needs admin access to the owner's
repos. GH_TOKEN overrides both.
Contributing
See CONTRIBUTING.md.