- Shell 91.6%
- Open Policy Agent 8.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .circleci | ||
| policies | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| .yamllint | ||
| LICENSE | ||
| README.md | ||
| renovate.json | ||
magicbeandev/ci
A small CircleCI orb for MagicBeanDev repos hosted on Forgejo. YAML plus a few bash scripts (curl, jq, git); no Python. Native CircleCI and Forgejo features first.
orbs:
mb: magicbeandev/[email protected]
Pinned dependencies: circleci/[email protected], circleci/[email protected].
Elements and examples
Job setup
Path-filtering for one pipeline event, then continues with shared-file + event-file.
Parameters: event-file (required), shared-file, mapping, base-revision, extra-params, force-full.
Mappings use the three-column form regex parameter value. extra-params is a JSON object; values
produced by the mapping win. force-full: true adds the pipeline parameter full: true.
- mb/setup:
event-file: .circleci/events/pr.yml
mapping: .circleci/mapping.conf
extra-params: '{"release_dry_run": true}'
Job fork-doorbell (forks project only)
Approval is sticky: a fork PR runs while it is open and carries ci: approved; the doorbell never removes the label.
It continues only when the PR (re-read from the Forgejo API) is open, from a fork, carries the label, and
refs/pull/N/head equals the PR head SHA. A SHA that already has a ci/circleci: gate status is skipped
(duplicate events) unless the newest ci: approved add on the timeline is newer than that status, so re-adding the
label reruns a commit and unrelated label changes do not. It posts pending and continues with shared-file +
event-file (default events/fork.yml) and the parameters fork_pr / fork_sha / full: true (an approved fork
run builds everything that applies; declare a boolean full parameter). Otherwise it halts cleanly, reason in the log.
No PR code runs.
The webhook body is only a hint. Pass it in the one documented form, webhook-body: << pipeline.trigger_parameters.webhook.body >>
(it reaches the script as WEBHOOK_BODY). The PR number is parsed defensively (JSON, a JSON-encoded string, or a
"number": N match); when the body is unusable or empty the doorbell scans the open fork PRs carrying the label and picks
the first whose head SHA has no gate status, so a body problem never silently means "no CI".
Parameters: repo (required), webhook-body, forgejo-url, shared-file, event-file. Context: forgejo-bot.
- mb/fork-doorbell:
repo: MyOrg/my-repo
webhook-body: << pipeline.trigger_parameters.webhook.body >>
context: forgejo-bot
Job fork-stale (forks project, daily schedule)
Expires approvals and unsticks runs, for open fork PRs carrying ci: approved. It fails closed: if the statuses or the
timeline cannot be read, the PR is skipped and nothing is changed.
- A
ci/circleci: gatestatus stillpendingafter 6 hours becomeserror("CI run did not finish; re-add ci: approved"). - When the newer of the last push and the last
ci: approvedadd (the PR creation if neither exists) is older thandays, the label is removed and one comment is posted ("Approval expired after N days without new commits. A maintainer can re-addci: approvedto resume CI.").
Non-fork PRs are ignored. Parameters: repo (required), days (14), forgejo-url. Context: forgejo-bot.
- mb/fork-stale: { repo: MyOrg/my-repo, context: forgejo-bot }
Job fork-report
One curl: posts ci/circleci: gate as success or failure on the fork head SHA.
Parameters: state, fork-sha, repo (required), forgejo-url. Two jobs are enough, no per-job variants:
- mb/fork-report: { name: fork-report-ok, state: success, fork-sha: << pipeline.parameters.fork_sha >>, repo: MyOrg/my-repo, requires: [fork-gate], context: forgejo-bot }
- mb/fork-report: { name: fork-report-fail, state: failure, fork-sha: << pipeline.parameters.fork_sha >>, repo: MyOrg/my-repo, requires: [{ fork-gate: [not_run, failed, canceled] }], context: forgejo-bot }
Job gate
Final required job; branch protection requires its status. No parameters.
- mb/gate: { requires: [build, test] }
Job release-sign / release-publish
Thin wrappers around the repo's own scripts/release.py, always taken from origin/main (the commit must be on main).
Both halt cleanly (no sign, no publish) when the tag is empty, or the workspace has release.skip or a release.env
with an empty TAG=. release-sign (context release-signing): workspace, dry-run; signs <workspace>/files.
release-publish (context forgejo-bot): tag (optional, default empty: TAG and REF are then read from
<workspace>/release.env, where nightly tags live), workspace, dry-run. Both need python3 in the image.
- mb/release-sign: { context: release-signing, requires: [package] }
- mb/release-publish: { tag: << pipeline.git.tag >>, context: forgejo-bot, requires: [mb/release-sign] }
Command setup-mise
Pinned mise release (sha256 per arch) unless present, mise trust, mise install, with a cache restored
always and saved only on main for non-fork runs. Parameters: mise-toml, git-branch, fork-pr.
- mb/setup-mise: { git-branch: << pipeline.git.branch >>, fork-pr: << pipeline.parameters.fork_pr >> }
Command save-cache-main
save_cache only when git-branch is main and fork-pr is 0. Parameters: key, paths, git-branch, fork-pr.
- mb/save-cache-main: { key: v1-deps-{{ checksum "go.sum" }}, paths: ~/go/pkg, git-branch: << pipeline.git.branch >> }
Command get-source
checkout, or in fork mode (fork-pr above 0) an anonymous fetch of refs/pull/N/head plus main's
history, failing unless the SHA equals fork-sha. Parameters: fork-pr, fork-sha, repo-url.
- mb/get-source: { fork-pr: << pipeline.parameters.fork_pr >>, fork-sha: << pipeline.parameters.fork_sha >>, repo-url: "https://git.magicbean.dev/MyOrg/my-repo.git" }
Command commit-subjects
Fails if a non-merge subject in base..HEAD is not a Conventional Commit (feat, fix, docs, style, refactor, perf,
test, build, ci, chore, revert; optional scope and !). Parameter: base (default origin/main).
- mb/commit-subjects
Command preview-comment
Validates preview-url.txt (at most 512 bytes, not a symlink, and strictly
https://output.circle-artifacts.com/output/job/<uuid>/artifacts/<n>/preview/.../index.html), then upserts one marker
comment authored by the bot. The URL is written by another job, so it is not bound to the commenting job. Residual risk:
a fork could point the link at another CircleCI artifact; it is still only a circle-artifacts link.
Parameters: repo (required), forgejo-url, pr, preview-url-file, commit-sha.
- mb/preview-comment: { repo: MyOrg/my-repo }
Command policy-check
Pinned conftest. Merges shared with each file in events-dir (yq, as path-filtering does) and tests every merged
result against the orb's Rego policies (shipped inside the orb), so contexts attached in event files are checked;
forks (optional) is tested as is. Parameters: shared, events-dir, forks. Needs yq (in cimg/base).
- mb/policy-check: { shared: .circleci/shared.yml, events-dir: .circleci/events, forks: .circleci/forks.yml }
Wiring a repo
One setup job per event in .circleci/config.yml, each with a native expression filter. Keep the
filters mutually exclusive; an event with no job does nothing.
version: 2.1
setup: true
orbs: { mb: magicbeandev/[email protected] }
workflows:
pr:
when:
and:
- equal: [pull_request, << pipeline.event.name >>]
- not: { equal: [main, << pipeline.git.branch >>] }
- equal: ["", << pipeline.git.tag >>]
jobs: [{ mb/setup: { event-file: .circleci/events/pr.yml, mapping: .circleci/mapping.conf } }]
main:
when:
and:
- equal: [push, << pipeline.event.name >>]
- equal: [main, << pipeline.git.branch >>]
- equal: ["", << pipeline.git.tag >>]
jobs: [{ mb/setup: { event-file: .circleci/events/main.yml } }]
release:
when:
and:
- equal: [push, << pipeline.event.name >>]
- matches: { pattern: "^v.*", value: << pipeline.git.tag >> }
jobs: [{ mb/setup: { event-file: .circleci/events/release.yml } }]
Route on pipeline.event.name (pull_request, push, schedule, api, custom_webhook), not pipeline.trigger_source
(deprecated, and not documented for Forgejo). Other events work the same way (schedule, api;
pipeline.schedule.name separates schedules). Repo layout: shared.yml (jobs), events/*.yml
(workflows), mapping.conf, forks.yml (the doorbell).
Forks
Forgejo has no native fork trigger, so the forks project uses CircleCI triggers:
- Add a custom webhook trigger to the forks project (config
.circleci/forks.ymlon main) and note its URL. In the Forgejo organization settings add a webhook to that URL and tick Pull request label and Pull request synchronized (JSON body). Forgejo only lets writers add labels, so addingci: approvedis the approval. - Add a daily schedule trigger to the same project for the stale sweep.
forks.ymlwires the webhook trigger tomb/fork-doorbelland the schedule trigger tomb/fork-stale(see thefork-doorbellexample), and the repo'sevents/fork.ymlruns the fork workflows usingfork_pr/fork_sha(get-source, thenfork-reportandgate).
Flow: label present + a push (synchronized) runs CI on that SHA; a repeat event for a SHA that already has a gate status is ignored; re-adding the label reruns the current commit; removing the label stops further runs; with no new commits for 14 days the sweep removes the label, so a maintainer must re-add it.
Development
bats testsruns the script tests (bats-core 1.13.0;tests/install-bats.shinstalls the pinned one). The doorbell tests shadowcurlwithtests/stubs/curl, which answers from Forgejo 17 shaped JSON.shellcheck -x src/scripts/*.sh tools/*.sh tests/*.sh tests/stubs/curlcircleci orb pack src > /tmp/orb.yml && circleci orb validate /tmp/orb.ymltools/validate.sh REPO_PATHmergesshared.ymlwith eachevents/*.ymlof a repo, inlines the locally packed orb asmb, runscircleci config validate, thenconftest testwithpolicies/.- CI (
.circleci/config.yml): orb-tools lint, pack, review (RC010 off: element names are hyphenated), shellcheck, bats, conftest verify.test-deploy.ymlpublishes a dev orb on branches and a semver onvX.Y.Ztags, contextorb-publishing.
Dependency updates (Renovate)
This repo runs a self-hosted Renovate from its own .circleci/config.yml: a renovate workflow that runs
only when the pipeline parameter renovate is true (the normal pipeline is skipped then). The job uses the
renovate/renovate image pinned by digest (the tag is in a comment) and runs renovate, with
RENOVATE_PLATFORM=forgejo, autodiscovery of MagicBeanDev/* and no custom scripts. This repo's own
renovate.json extends config:recommended and docker:pinDigests, waits 7 days (minimumReleaseAge) and
lets the circleci manager update the orbs and images in .circleci/ and src/.
Onboarding flow (the standard one):
- Each discovered repo gets a "Configure Renovate" PR (
RENOVATE_ONBOARDING=true). Itsrenovate.jsoncomes fromRENOVATE_ONBOARDING_CONFIG:config:recommended,minimumReleaseAge7 days, dockerpinDigests, minor and patch updates grouped,ignorePathstools.lockand**/third_party/**, labeldependencies. Maintainers can edit the PR before merging. RENOVATE_REQUIRE_CONFIG=required: until the maintainers merge that PR, Renovate does nothing in the repo.
Setup:
- Bot token. Create a Forgejo bot user (it is also the git author,
RENOVATE_GIT_AUTHOR) and a token with these scopes, per Renovate's Forgejo docs: repository read/write, user read, issue read/write, organization read. Give the bot write access to the repositories to scan. - Context
renovate. In CircleCI create a contextrenovatewithRENOVATE_TOKEN, restricted to themainbranch so no other branch or fork can read the token. - Schedule. Add a weekly (not nightly) schedule trigger to this project on
mainthat sends the pipeline parameterrenovate: true.