Gem Version Build Status Pull Requests Commits since latest

Purpose

Cimas handles synchronizing CI configuration across multiple repositories.

When there are many repositories that need to share identical or similar CI (continuous integration) configuration, it is not only cumbersome but error prone to keep these configuration files in sync.

For example, adding new value to the CI test matrix for multiple repositories should not itself be a chore.

Note
This problem can technically be mitigated if we can externalize and consolidate CI configuration outside of data repositories. However, most CI solutions don’t support external configuration (i.e. Travis, GitHub Actions, Appveyor). For GitHub, see: GHA external workflow, GHA calling another action

History

Cimas was built to synchronize CI configuration across the the Metanorma and Relaton repositories.

We had a hard time managing them individually!

Installation

Cimas (and its CLI command, cimas) relies on Git. Please ensure git is installed on your system.

Installing is as easy as:

gem install cimas

Or, by specifying it in your Gemfile if you’re using Bundler.

Concepts

CI configuration file

file to configure CI behavior of a repository

CI configuration master file

CI configuration file that applies to one or more target repositories

CI configuration master directory

directory (or a repo) where master CI configuration files reside

Cimas working area

local directory where Cimas works within

Cimas managed repository

repository with CI configuration managed by Cimas

Demonstration

The metanorma-build-scripts repository contains Cimas configuration under cimas-config/ for the Metanorma repositories.

  • cimas-config/: the CI master configuration directory

  • cimas-config/cimas.yml: the Cimas configuration file

Have a look to see how it’s being used!

Prerequisites

Setting up the CI configuration master directory

You need to first create a “CI configuration master directory” to contain master files for CI configuration. It is advisable to store the directory in Git for better version management.

For example, with Metanorma repositories, the CI master configuration directory is at metanorma/ci under cimas-config/. (Historical note: prior to 2024 this lived in metanorma-build-scripts; older documentation may still reference that location.)

Setting the GITHUB_TOKEN environment variable

Cimas needs a GitHub token to look up per-repo visibility (public vs. private) when the CI configuration file uses visibility-driven template selection (see the metanorma/metanorma-docker template selection in metanorma/ci#347 Option B).

Without the token, cimas sync prints a warning per repo and defaults to private template picks — safe (won’t clobber public repos with private templates), but the sync is inaccurate for public repos.

Set the token before running any sub-command that touches remotes or inspects visibility:

export GITHUB_TOKEN=$(gh auth token)

gh auth token reuses the token the local gh CLI is authenticated with. Alternatively, export an explicit PAT with repo scope.

Usage

Command line

Cimas works through the cimas executable. cimas provides the following sub-commands.

  • setup

  • sync

  • diff

  • push

  • pull

  • open-prs

  • for-each

  • cleanup-merged-prs

  • cleanup-closed-prs

  • cleanup-orphan-files

  • release-preflight

Run cimas help to list every sub-command and cimas help <command> for a command’s flags. Running cimas with no sub-command prints help.

Global options

  • --dry-run: Skips destructive/remote operations (branches, pushes, PR opens, deletes); prints what would be done instead. Applies to sync, pull, push, open-prs, cleanup-*.

  • -v / --verbose.

Global options are given with the sub-command, like any other flag (cimas push --dry-run …​). Running cimas with no sub-command prints help; cimas help <command> shows a command’s flags.

Note

Do not confuse --dry-run with -d. -d is a sub-command option on sync — it stands for --master-path (the CI configuration master directory), not for dry-run. See per-sub-command flag lists below.

Warning

push, open-prs, cleanup-merged-prs, cleanup-closed-prs, for-each and cleanup-orphan-files --push-after refuse to run without -g. These commands mutate remote state — provisioning branches, opening PRs, deleting branches, or (for for-each) running arbitrary shell in every repo — and without -g their scope is every repository in cimas.yml, which fans a wave out to repos unrelated to its purpose. Pass -g <group(s)> or -g <repo-name> to scope; -g all targets the whole fleet deliberately. An empty -g (e.g. -g '') or a group that resolves to zero repositories is also refused — a silent 0-repo no-op is never what you meant. Every guarded command prints a pre-flight Scope for <command>: N repo(s): …​ line so the blast radius is visible before anything happens. Local-only commands (sync, pull, diff) are unaffected and still default to all repositories.

cimas setup

The setup sub-command sets up the Cimas working area/directory with Git repos cloned as described in your Cimas configuration file (default: cimas.yml).

This command must be run before any other sub-commands are run, and must be run when the repository addresses change within the Cimas configuration file (it will prompt you).

cimas setup -f {cimas-config-file} -r {cimas-working-area}

# e.g.
# cimas setup -f cimas.yml -r ~/src/cimas-wd
Note
This sub-command is considered “safe” as it does not alter the state of existing Git repositories within the Cimas working area.

cimas pull

The pull sub-command resets all the Git repositories to the original branch key set per-repo.

This command should be run before the cimas sync command to clean up state.

cimas pull -f {cimas-config-file} -r {cimas-working-area}

# e.g.
# cimas pull -f cimas.yml -r ~/src/cimas-wd
Note
This sub-command is considered “destructive” as it resets states of all Git repositories in the Cimas working area.

cimas sync

The sync sub-command places the necessary CI configuration files, as described in the Cimas configuration file, into the desired repositories.

After copying the files, Cimas will also stage the changed files to Git.

This command must be run before the cimas push command.

cimas sync -f {cimas-config-file} -r {cimas-working-area} \
  -d {cimas-master-config-dir}

# e.g.
# cimas sync -f cimas.yml -r ~/src/cimas-wd \
#   -d ~/src/cimas-config
Note
This sub-command is considered “destructive” as it resets states of all Git repositories in the Cimas working area before adding CI configuration files. But you can pass -k argument to not 'drop' state of the repo before sync
Proposed extension — drift-audit / opt-out detection (Gap 3, not yet implemented)
Note
This subsection documents a proposed drift-audit capability, not currently implemented. See metanorma/ci#300 (Gap 3) for the full design discussion. The naming question (separate cimas drift-audit subcommand vs. a cimas sync --drift-audit flag) is part of the open design.

cimas sync today applies templates blindly: if a cimas.yml entry maps a downstream file to a template, the file is overwritten on every sync — even when the downstream repo has deliberately opted out by replacing the template with custom content. The opt-out is silent; the maintainer rediscovers it only when their override gets clobbered.

Concrete example: metanorma/coradoc/.github/workflows/rake.yml was replaced in d91d06d ("fix(ci): replace metanorma reusable workflow with direct rake job") with a direct in-file matrix-test job. Until tonight’s metanorma/ci#318 (the cimas-config/cimas.yml one-liner removing coradoc’s rake.yml mapping), every cimas sync run silently planned to clobber that opt-out. There is no equivalent automation for the next opt-out that gets introduced.

The proposed drift-audit (subcommand or flag, naming TBD) runs before every cimas sync and classifies each planned diff into:

  • Gap 1 signal — diff removes with: block lines absent from any template the repo maps to. Likely indicates a per-repo with: override that the schema extension (above) hasn’t yet been used to capture.

  • Gap 2 signal — structural diff (full jobs: block differs) AND the downstream file references a local ./.github/workflows/ path. Likely a monorepo or other structural fork.

  • Gap 3 signal — downstream file lacks the # Auto-generated by Cimas: Do not edit it manually! header AND the structural diff to the assigned template is large. Likely an opt-out.

  • Category D (benign) — diff adds template-resident lines absent from the downstream file. Standard template-forward drift; no hazard.

The audit reports Gap 1, 2, and 3 signals to the maintainer with the proposed cimas.yml or template edit that would resolve each, and exits non-zero on Gap 1 or Gap 3 signals, blocking cimas push until the maintainer either applies the remediation or explicitly overrides the audit.

The drift-audit serves as a forcing function against the silent-overwrite class of failures — opt-outs and per-repo overrides surface as actionable signals rather than getting clobbered. Naming and exact integration point (separate subcommand vs. flag on cimas sync) are open per `#300’s discussion section.

Until either is implemented, opt-out hygiene is a manual diff-review burden on every sync run.

cimas diff

The diff sub-command provides a consolidated diff output between the repositories in the Cimas working area and remote. This is useful prior to running the cimas push command.

After copying the files, Cimas will also stage the changed files to Git.

This command must be run before the cimas push command.

cimas diff -f {cimas-config-file} -r {cimas-working-area} \
  -d {cimas-master-config-dir}

# e.g.
# cimas diff -f cimas.yml -r ~/src/cimas-wd \
#   -d ~/src/cimas-config
Note
This sub-command is considered “safe” as it does not alter state of Git repositories.

cimas push

The push sub-command:

  • commits the changes made by the sync sub-command in a new branch;

  • pushes the new branch to the first Git remote.

This command must be run before the cimas open-prs command as the branches need to be pushed before pull-requests can be opened against them.

cimas push -f {cimas-config-file} -r {cimas-working-area} \
  -b {new-branch-for-commit} \
  -m {commit-message} \

# e.g.
# cimas push -f cimas.yml -r ~/src/cimas-wd \
#  -b my-new-ci-branch \
#  -m 'My commit message' \
#  [-g {group1,group2,...}]
Note
This sub-command is considered “destructive” as it alters the state of all Git repositories in the Cimas working area by adding commits and branches.

cimas open-prs

The open-prs sub-command:

  • opens Pull Requests for all the specified repositories of the given branch;

  • creates Pull Request Review Requests for the created Pull Requests (if reviewers are set in cimas.yaml:settings or via the -w option);

  • assigns the created Pull Requests to assignees (if assignees are set in cimas.yaml:settings or via the -a option).

Since this command depends on GitHub privileged functionality, you must supply your GitHub Personal Access Token (PAT) via the GITHUB_TOKEN environment variable.

GITHUB_TOKEN=deadbeefdeadbeef; \
cimas open-prs -f {cimas-config-file} -r {cimas-working-area} \
  -b {new-branch-to-pr} \
  -m {pr-message} \

# e.g.
# cimas open-prs -f cimas.yml -r ~/src/cimas-wd \
#   -b my-new-ci-branch \
#   -m 'My pull-request message' \
#   [-w {reviewer1,reviewer2...}] \
#   [-a {assignee1,assignee2...}] \
#   [-g {group1,group2,...}]
Note
This sub-command is considered “destructive” as it alters the state of GitHub repositories by creating pull requests.
--supersede-stale flag (Gap 4, implemented 2026-06-30 in cheaper form)

When cimas open-prs runs against a repo where an earlier cimas-sync wave’s PR is still open (i.e. unmerged from a previous wave), the result without intervention is stacked PRs on the same repo carrying overlapping changes, with no automated relationship between them. The previous wave’s PR (e.g. PR#5 on the cimas-sync-2026-06-15 branch) and the new wave’s PR (e.g. PR#10 on cimas-sync-2026-06-29) coexist independently; the reviewer either merges both (redundant churn, possible conflicts), merges only the latest (`PR#5’s review thread gets orphaned), or freezes (the most common outcome). The pattern bites most on inactive-maintainer repos where PRs can sit for weeks or months before notice.

The --supersede-stale flag (opt-in) implements the cheaper-variant design discussed in metanorma/ci#300 (Gap 4). For each target repo, when set:

  1. Before opening the new PR, lists existing open PRs whose head branch starts with cimas-sync- (i.e. previous wave PRs that never merged).

  2. Prepends a Supersedes #X, #Y from prior cimas-sync waves. line to the new PR’s body, naming the superseded PRs.

  3. After the new PR is opened, labels each superseded PR with superseded-by-#N (where N is the new PR’s number) and posts a comment linking the new PR.

  4. Does NOT auto-close the superseded PRs — the reviewer keeps authority over the close decision.

cimas open-prs --supersede-stale \
  -r ~/src/cimas-wd-2026-06-29 \
  -f cimas.yml \
  -b cimas-sync-2026-06-29 \
  -m "Cimas sync 2026-06-29: ..." \
  --body-file /tmp/wave-body.md \
  -g processor

A future PR may layer a strict-superset diff gate on top (only supersede when the new wave’s diff strictly contains the old PR’s diff, so different-intent waves stay parallel). For now the simpler cheaper-variant is sufficient — the label and comment make the supersede relationship visible and let the reviewer make the final close call.

A more elegant single-branch-per-repo alternative (rebase the existing cimas-sync branch in-place rather than opening new PRs) is noted in metanorma/ci#300 as a possible Phase-B candidate — bigger mental-model shift, defer until the revival settles.

--flatten-stale flag (Gap 4 full, implemented 2026-07-04)

--flatten-stale is the full Gap 4 shape: same detection as --supersede-stale but also auto-closes the superseded PRs. Implies --supersede-stale (setting --flatten-stale alone activates both detection AND auto-close).

Use this when you’re confident the new wave’s content strictly supersedes the older waves' — which is the standard case for cimas-sync waves, since every wave regenerates the same file set from cimas.yml. If the older PR contained content that should be preserved (e.g. a mid-wave hand-edit), rebase that branch elsewhere and reopen before running the new wave.

Behavioural delta vs --supersede-stale:

  • Superseded PRs get labelled superseded-closed-by-#N (instead of superseded-by-#N).

  • The comment on each superseded PR names auto-closure and points at the new PR.

  • Each superseded PR is closed via github_client.close_pull_request.

cimas open-prs --flatten-stale \
  -r ~/src/cimas-wd-2026-06-29 \
  -f cimas.yml \
  -b cimas-sync-2026-06-29 \
  -m "Cimas sync 2026-06-29: ..." \
  --body-file /tmp/wave-body.md \
  -g processor

The strict-superset diff gate mentioned above for --supersede-stale’s follow-up is orthogonal to `--flatten-stale — the flatten variant assumes the wave-regeneration invariant rather than checking it per-PR. If the invariant is broken (e.g. a wave that intentionally covers a narrower subset of files), stick with --supersede-stale and close manually.

cimas for-each

The for-each sub-command executes an arbitrary shell command in each repo’s working copy.

Each repo’s command exit status is reported per-repo ([ERROR] '<cmd>' failed in <repo> (exit N)); if any command failed, for-each lists the failing repos at the end and exits non-zero, so CI can detect it.

There is no strict requirement to use this command during CI configuration update. You can use it on demand, for example if some CI configuration files were removed

cimas sync -f {cimas-config-file} -r {cimas-working-area} \
  -d {cimas-master-config-dir}

# e.g.
# cimas for-each -f cimas.yml -r ~/src/cimas-wd \
#   -c "git rm .github/workflows/windows.yml"

End-to-end workflow: drift audit → preview → push

The full sequence for a “sync + push a wave of drift corrections” operation is setupsyncdiffpush. Each sub-command has its own required flags, so the full sequence is worth banking in one place.

Prerequisites for this workflow:

  • GITHUB_TOKEN exported (see prerequisites above).

  • A checkout of the CI configuration master directory (e.g. metanorma/ci for Metanorma). Below assume it lives at ../ci/cimas-config/.

  • A fresh work-dir (e.g. ../cimas-wd-YYYY-MM-DD).

Step 1 — create the work-dir.

mkdir ../cimas-wd-2026-07-22

Step 2 — setup: clone every mapped repo into the work-dir.

cimas setup \
  -r ../cimas-wd-2026-07-22 \
  -f ../ci/cimas-config/cimas.yml

Setup does not need -d (it only clones; it doesn’t apply templates).

Step 3 — sync: apply templates to each cloned repo locally.

cimas sync \
  -r ../cimas-wd-2026-07-22 \
  -f ../ci/cimas-config/cimas.yml \
  -d ../ci/cimas-config

Both -r (work-dir), -f (cimas.yml), and -d (master path) are required for sync. Missing -d produces [ERROR] config_master_path not set, aborting.

Step 4 — cimas diff: preview the aggregated diff. This is the primary preview mechanism.

cimas diff \
  -r ../cimas-wd-2026-07-22 \
  -f ../ci/cimas-config/cimas.yml \
  -d ../ci/cimas-config

Same three flags as sync. Prints git diff output per drifted repo. This is the decision surface — read it and decide which classes to propagate, and which should become opt-out entries in cimas.yml. Repos with no drift show no diff at all.

Quick count of repos with actual drift (post-sync, pre-push):

cd ../cimas-wd-2026-07-22 && for d in */; do
  [ -n "$(git -C "$d" status --porcelain 2>/dev/null)" ] && echo "${d%/}"
done | tee /tmp/drifted-repos.txt | wc -l

Useful as a pre-push census: cimas push skips no-drift repos with a "Skipping no-op push" line, so this one-liner gives you the drifted subset up front, before any branch is provisioned.

Note

The -b and -m push options are both required and validated up front — cimas push exits 1 before touching any repo if either is missing. A cimas diff preview needs neither.

Step 5 — real push (only after diff review).

cimas push \
  -r ../cimas-wd-2026-07-22 \
  -f ../ci/cimas-config/cimas.yml \
  -b cimas-sync-2026-07-22 \
  -m "cimas: sync <date> drift-audit findings"

Creates cimas-sync-2026-07-22 on each mapped repo and pushes it. Does NOT open PRs — that’s cimas open-prs as a separate step.

Important

cimas push skips repos with no drift: each is logged as Skipping no-op push to <repo> (no drift) and no branch is provisioned for it. The push summary separates Pushed with drift from Skipped (no drift), so watchers only get notified for repos that actually changed.

The next-stage cimas open-prs tolerates these skips: a repo whose wave branch is absent on the remote is skipped with a WARNING, and an empty PR (No commits between) is skipped too.

cimas push --dry-run prints the actions it would take without executing them; cimas diff remains the primary preview of what actually changes.

Step 6 — open PRs (optional; only for the drift-having branches).

See the cimas open-prs section above for the full flag list.

Table 1. Required-flag matrix per sub-command
Sub-command Required flags

setup

-r -f

sync

-r -f -d

diff

-r -f

pull

-r -f

push

-r -f -b -m -g

open-prs

-r -f -b (merge branch) -m (PR title) -g

for-each

-r -f -c -g

cleanup-merged-prs

-r -f -b -g

cleanup-closed-prs

-r -f -g

cleanup-orphan-files

-r -f -g (plus -b -m under --push-after)

release-preflight

-r -f --repo

Missing required flags fail fast with Error: <command>: missing required option(s): …​ and exit 1 before any repo is touched. -g is required by the scope guard on every remote-mutating sub-command (see the warning above).

Table 2. Gotchas summary
Gotcha Symptom

Missing -d on sync

[ERROR] config_master_path not set, aborting.

Missing -b on push/open-prs

Error: Missing -b/--push-branch value

Missing GITHUB_TOKEN

[WARNING] Visibility fetch failed …​; defaulting to \`private\ (safer).` — sync is inaccurate for public repos.

Global flag placed before the sub-command

Usage help is printed instead of running — global flags go after the sub-command (cimas push --dry-run …​).

Config without a repositories: section

Error at startup: no `repositories: section in …​ — nothing to operate on`.

Configuration

General

Cimas relies on reading a Cimas configuration file (default: cimas.yml) that specifies:

  • repository settings;

  • group settings; and

  • Cimas behavior

This YAML file needs to be in the following structure:

---
settings:
  {option-key}: {option-value}
  ...

repositories:
  {repo-name}:
    remote: {remote-name}
    branch: {branch-name}
    files:
      {CI-file-target-location}: {CI-configuration-master-file-location}
    ...
  ...

groups:
  {group-name}:
    - {repo-name}
    - ...

EXAMPLE: See metanorma/metanorma-build-scripts/cimas-config/cimas.yml for a working configuration.

settings

The settings object specifies run-time configuration. These options are merged with the command-line options, which have higher priority.

Syntax:

settings:
  {option-key}: {option-value}
  ...

The following options are available:

  • reviewers takes an array of GitHub user names as PR reviewers. This is only relevant to the cimas open-prs sub-command.

  • assignees takes an array of GitHub user names as assignees to PRs. This is only relevant to the cimas open-prs sub-command.

EXAMPLE: This example comes from metanorma/metanorma-build-scripts/cimas-config/cimas.yml.

settings:
  reviewers:
    - opoudjis
    - ronaldtse

repositories

The repositories object specifies all Git repositories that are managed by Cimas under this configuration file.

For example, when the cimas setup command is run, all of these repositories will be cloned under the Cimas working area.

Each repository is represented by a key under the repositories object.

  {repo-name-1}:
    remote: {remote-name}
    branch: {branch-name}
    files:
      {CI-file-target-location-1}: {CI-configuration-master-file-location-1}
      {CI-file-target-location-2}: {CI-configuration-master-file-location-2}

These attributes are mandatory for each repository:

  • remote: the remote Git location of this repository (i.e. where git clone can find this repository). SSH and HTTPS paths are supported. Single valued.

  • branch: the source branch and eventual branch to commit to (where a PR should be created against). Single valued.

  • files: composed of key value pairs of the “target file location within the repository” to the “master file location within the configuration master directory”. Multiple files are supported.

Syntax:

repositories:
  {repo-name-1}:
    remote: {remote-name}
    branch: {branch-name}
    files:
      {CI-file-target-location-1}: {CI-configuration-master-file-location-1}
      {CI-file-target-location-2}: {CI-configuration-master-file-location-2}
  {repo-name-2}:
    remote: {remote-name}
    branch: {branch-name}
    files:
      {CI-file-target-location-3}: {CI-configuration-master-file-location-3}
    ...
  ...

EXAMPLE: This example comes from metanorma/metanorma-build-scripts/cimas-config/cimas.yml.

repositories:
  metanorma-model-gb:
    remote: ssh://git@github.com/metanorma/metanorma-model-gb
    branch: master
    files:
      .github/workflows/macos.yml: gh-actions/model/macos.yml
      .github/workflows/ubuntu.yml: gh-actions/model/ubuntu.yml
      .github/workflows/windows.yml: gh-actions/model/windows.yml

Proposed schema extension — per-repo with: rendering (Gap 1, not yet implemented)

Note
This subsection documents a proposed extension to the files: schema, not currently implemented. See metanorma/ci#300 (Gap 1) for the full design discussion, including the concrete renderer change and the first intended user (private-fonts: true for metanorma).

The current files: schema maps a downstream-file path to a template path as a single string. This works for cases where the template is fully self-contained, but breaks when the synced file needs a per-repo value that the shared template can’t anticipate (e.g. metanorma’s `rake.yml needs private-fonts: true, but gh-actions/inkscape/rake.yml has no slot for it).

The proposed extension keeps the existing string form valid (fully backward compatible) and adds an optional map form for entries that need per-repo with: rendering:

  metanorma:
    remote: ssh://git@github.com/metanorma/metanorma
    branch: main
    files:
      # Existing string form — kept verbatim, no per-repo with:
      .rubocop.yml: gh-actions/master/.rubocop.yml
      .github/workflows/release.yml: gh-actions/master/release.yml

      # Proposed map form — template + per-repo with: values
      .github/workflows/rake.yml:
        template: gh-actions/master/rake.yml
        with:
          private-fonts: true

The renderer would route on the value’s type: strings render as today; maps extract template: as the path-to-render and pass with: to ERB as a local variable for injection into the template’s with: block. Templates that wish to support per-repo with: rendering gain an ERB injection block such as:

jobs:
  rake:
    uses: metanorma/ci/.github/workflows/generic-rake.yml@main
<% if defined?(file_with) && file_with.any? -%>
    with:
<% file_with.each do |k, v| -%>
      <%= k %>: <%= v.inspect %>
<% end -%>
<% end -%>
    secrets:
      pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}

Until this lands, per-repo with: overrides have to be either inlined in each downstream file (and clobbered by every cimas-sync) or kept out-of-band as documentation, neither of which is sustainable.

Proposed schema extension — monorepo sub-template family (Gap 2, not yet implemented)

Note
This subsection documents a proposed extension to support monorepo-shaped repositories, not currently implemented. See metanorma/ci#300 (Gap 2) for the full design discussion. The reusable-workflow side of the work — adding monorepo / gem_directory inputs to the relevant metanorma/ci/.github/workflows/*.yml reusable workflows — is a coupled dependency tracked separately and is part of the open design question.

The existing gh-actions/master/ and per-flavour template families (gh-actions/inkscape/, gh-actions/xml2rfc/, gh-actions/graphviz/, gh-actions/libreoffice/) all assume a one-repo-one-gem layout: a single .gemspec at the repo root, one Gemfile, one rakefile. Monorepos that have been adopted across the metanorma org since the templates were written break this assumption.

Concrete example: metanorma/pubid is a monorepo of pubid-* gems under gems/. Its .github/workflows/rake.yml has been forked structurally:

jobs:
  rake:
    uses: ./.github/workflows/generic-rake.yml   # local path, not metanorma/ci
    with:
      monorepo: true
      gem_directory: gems
    secrets:
      pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}

Note the uses: points at a local copy of generic-rake.yml, not at metanorma/ci. That’s because the metanorma/ci reusable workflow doesn’t currently accept monorepo / gem_directory inputs, so the monorepo had to fork the workflow locally to extend it. The cimas template family then has no way to express either the inputs or the structural shape, so monorepos sit outside cimas-sync entirely.

The proposed remediation pairs a new template family with reusable-workflow extensions:

  1. A new cimas-config/gh-actions/monorepo/.yml sub-template family in metanorma/ci, paralleling the existing gh-actions/master/ shape but emitting with: { monorepo: true, gem_directory: <path> } blocks.

  2. Reusable-workflow extensions to metanorma/ci/.github/workflows/generic-rake.yml (and siblings as needed) to accept monorepo / gem_directory inputs and dispatch the matrix correctly per-gem rather than per-repo.

  3. Monorepo repos' cimas.yml entries reference the new template family and (per Gap 1’s proposed schema extension) declare the per-monorepo with: values.

The Gap 1 schema extension above is a prerequisite — without per-repo with: rendering, the new template family can’t express the variable gem_directory values across different monorepos. So Gap 2 layers on top of Gap 1, not independently.

Until this lands, monorepo repos remain outside cimas-sync’s scope and maintain their CI configuration by hand.

groups

Cimas offers “grouping” functionality to allow you to work with groups of repositories. This is useful if your repositories fall into different categories, e.g. repositories for Ruby code vs C code that have different build routines.

There is a default group of all which applies if no group is specified.

Groups under the groups key are collections of repository names. Each group is represented by a key of the group’s name, with names of its repositories as array content.

One repository may belong to multiple groups. Groups have no bearing on what files to synchronize; the files must be specified per repository in the configuration file under the repositories section.

The -g switch in the various commands directly refer to the {group-name} specified in the configuration file.

Syntax:

groups:
  {group-name-1}:
    - {repo-name-1}
    - {repo-name-2}
    ...
  ...

EXAMPLE: This example comes from metanorma/metanorma-build-scripts/cimas-config/cimas.yml.

groups:
  model:
  - metanorma-model-iso
  - metanorma-model-gb
  - metanorma-model-standoc

Patches

In addition to full-file sync via files:, cimas supports in-place line-level edits via a top-level patches: section. A patch is a named regex find/replace that is applied to files already present in each target repo. Patches are typically used for values (like a minimum Ruby version) that live inside per-repo unique files (like `.gemspec`s) and therefore cannot be copied wholesale.

patches:
  ruby_version:
    files:
      - "*.gemspec"
    find: 'spec\.required_ruby_version\s*=.*'
    replace: 'spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")'
    groups: [processor, pubid, model]

Each patch entry supports these keys:

  • files — array of glob patterns, evaluated from each target repo’s root. Use */.ext for recursive matching.

  • find — Ruby regex (as a string). Compiled with Regexp.new.

  • replace — replacement string passed to gsub. Backreferences (\\1, etc.) work.

  • groups — which groups: (defined elsewhere in the config) the patch applies to. A repo receives the patch if it appears in any listed group.

Patches are applied during cimas sync, immediately after the file-copy phase, and the resulting changes are staged via git add so they flow through cimas push and cimas open-prs the same way as file-copy changes.

If a patch’s files glob matches nothing, or the find regex matches no content in a file, cimas logs a warning but does not fail — this makes it safe to leave stale patches in config without blocking other repos.

Unlike file-copy sync, patches do not prepend the "Auto-generated by Cimas" header, since they modify existing files in place rather than replacing them.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

The test suite is fully offline: git-touching specs run against real repositories created in temp directories, with a local bare repository as the push remote — no network and no GitHub access are needed. This includes an end-to-end lifecycle spec (syncdiffpush).

Architecture overview (for contributors)

  • exe/cimas boots Cimas::Cli::Runner (Thor): the CLI surface — option parsing, help (cimas help [COMMAND]), and one-line delegation per sub-command.

  • Cimas::Cli::Command is the orchestrator. One COMMANDS registry entry per subcommand drives dispatch behavior: the remote-mutating classification (scope guard + blast-radius announcement) and the required options (fail-fast validation).

  • Domain models are value objects over the cimas.yml sections: Cimas::Repository (repositories:), Cimas::Patch (patches:).

  • Seams: Cimas::WorkingCopy is the git seam (all git-gem knowledge lives there — subcommands never call the git gem directly); Cimas::GitHub is the Octokit boundary (client, remote→slug mapping, visibility fallback); Cimas::OrphanFiles is the pure orphan-detection logic; Cimas::ReleasePreflight is the extracted release check-runner.

  • Cimas::GENERATED_HEADER is the single definition of the generated-file header shared by the writer and orphan detection.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/metanorma/cimas. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

Code of Conduct

Everyone interacting in the Cimas project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.