cimas 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.adoc CHANGED
@@ -1,7 +1,8 @@
1
1
  = Cimas ("`Continuous integration master`")
2
2
 
3
3
  image:https://img.shields.io/gem/v/cimas.svg["Gem Version", link="https://rubygems.org/gems/cimas"]
4
- image:https://codeclimate.com/github/metanorma/cimas/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/metanorma/cimas"]
4
+ image:https://github.com/metanorma/cimas/workflows/rake/badge.svg["Build Status", link="https://github.com/metanorma/cimas/actions?workflow=rake"]
5
+ // image:https://codeclimate.com/github/metanorma/cimas/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/metanorma/cimas"]
5
6
  image:https://img.shields.io/github/issues-pr-raw/metanorma/cimas.svg["Pull Requests", link="https://github.com/metanorma/cimas/pulls"]
6
7
  image:https://img.shields.io/github/commits-since/metanorma/cimas/latest.svg["Commits since latest",link="https://github.com/metanorma/cimas/releases"]
7
8
 
@@ -79,11 +80,35 @@ contain master files for CI configuration.
79
80
  It is advisable to store the directory in Git for better version management.
80
81
 
81
82
  For example, with https://github.com/metanorma[Metanorma] repositories,
82
- the CI master configuration directory is placed in the
83
- https://github.com/metanorma/metanorma-build-scripts[`metanorma-build-scripts`]
84
- repository under `cimas-config/`.
83
+ the CI master configuration directory is at
84
+ https://github.com/metanorma/ci[`metanorma/ci`] under `cimas-config/`.
85
+ (Historical note: prior to 2024 this lived in
86
+ https://github.com/metanorma/metanorma-build-scripts[`metanorma-build-scripts`];
87
+ older documentation may still reference that location.)
85
88
 
86
89
 
90
+ === Setting the `GITHUB_TOKEN` environment variable
91
+
92
+ Cimas needs a GitHub token to look up per-repo visibility (public vs.
93
+ private) when the CI configuration file uses visibility-driven template
94
+ selection (see the `metanorma/metanorma-docker` template selection in
95
+ https://github.com/metanorma/ci/issues/347[`metanorma/ci#347`] Option B).
96
+
97
+ Without the token, `cimas sync` prints a warning per repo and defaults
98
+ to `private` template picks — safe (won't clobber public repos with
99
+ private templates), but the sync is inaccurate for public repos.
100
+
101
+ Set the token before running any sub-command that touches remotes or
102
+ inspects visibility:
103
+
104
+ [source,sh]
105
+ ----
106
+ export GITHUB_TOKEN=$(gh auth token)
107
+ ----
108
+
109
+ `gh auth token` reuses the token the local `gh` CLI is authenticated
110
+ with. Alternatively, export an explicit PAT with `repo` scope.
111
+
87
112
 
88
113
  == Usage
89
114
 
@@ -98,15 +123,50 @@ Cimas works through the `cimas` executable. `cimas` provides the following sub-c
98
123
  * `push`
99
124
  * `pull`
100
125
  * `open-prs`
101
- * `lint` (disabled right now)
102
-
103
-
104
- ==== Sub-command options
105
-
106
- These options work across all sub-commands:
107
-
108
- * `-d` or `--dry-run`: Does not perform any destructive behavior.
109
- * `-v` (disabled for now)
126
+ * `for-each`
127
+ * `cleanup-merged-prs`
128
+ * `cleanup-closed-prs`
129
+ * `cleanup-orphan-files`
130
+ * `release-preflight`
131
+
132
+ Run `cimas help` to list every sub-command and `cimas help <command>`
133
+ for a command's flags. Running `cimas` with no sub-command prints help.
134
+
135
+ ==== Global options
136
+
137
+ * `--dry-run`: Skips destructive/remote operations (branches, pushes,
138
+ PR opens, deletes); prints what would be done instead. Applies to
139
+ `sync`, `pull`, `push`, `open-prs`, `cleanup-*`.
140
+ * `-v` / `--verbose`.
141
+
142
+ Global options are given with the sub-command, like any other flag
143
+ (`cimas push --dry-run ...`). Running `cimas` with no sub-command
144
+ prints help; `cimas help <command>` shows a command's flags.
145
+
146
+ [NOTE]
147
+ ====
148
+ Do not confuse `--dry-run` with `-d`. `-d` is a **sub-command** option
149
+ on `sync` — it stands for `--master-path` (the CI configuration master
150
+ directory), not for dry-run. See per-sub-command flag lists below.
151
+ ====
152
+
153
+ [WARNING]
154
+ ====
155
+ `push`, `open-prs`, `cleanup-merged-prs`, `cleanup-closed-prs`,
156
+ `for-each` and `cleanup-orphan-files --push-after` **refuse to run
157
+ without `-g`**.
158
+ These commands mutate remote state — provisioning branches, opening PRs,
159
+ deleting branches, or (for `for-each`) running arbitrary shell in every
160
+ repo — and without `-g` their scope is every repository in `cimas.yml`,
161
+ which fans a wave out to repos unrelated to its purpose.
162
+ Pass `-g <group(s)>` or `-g <repo-name>` to scope; `-g all` targets the
163
+ whole fleet deliberately. An empty `-g` (e.g. `-g ''`) or a group that
164
+ resolves to zero repositories is also refused — a silent 0-repo no-op is
165
+ never what you meant. Every guarded command prints a pre-flight
166
+ `Scope for <command>: N repo(s): ...` line so the blast radius is
167
+ visible before anything happens. Local-only commands (`sync`, `pull`,
168
+ `diff`) are unaffected and still default to all repositories.
169
+ ====
110
170
 
111
171
 
112
172
  ==== `cimas setup`
@@ -175,7 +235,29 @@ cimas sync -f {cimas-config-file} -r {cimas-working-area} \
175
235
 
176
236
  NOTE: This sub-command is considered "`destructive`" as it
177
237
  resets states of all Git repositories in the Cimas working area
178
- before adding CI configuration files.
238
+ before adding CI configuration files. But you can pass -k argument
239
+ to not 'drop' state of the repo before sync
240
+
241
+ ===== Proposed extension — drift-audit / opt-out detection (Gap 3, not yet implemented)
242
+
243
+ NOTE: This subsection documents a **proposed** drift-audit capability, not currently implemented. See https://github.com/metanorma/ci/issues/300[`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.
244
+
245
+ `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.
246
+
247
+ Concrete example: `metanorma/coradoc/.github/workflows/rake.yml` was replaced in https://github.com/metanorma/coradoc/commit/d91d06d[`d91d06d`] (*"fix(ci): replace metanorma reusable workflow with direct rake job"*) with a direct in-file matrix-test job. Until tonight's https://github.com/metanorma/ci/pull/318[`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.
248
+
249
+ The proposed drift-audit (subcommand or flag, naming TBD) runs before every `cimas sync` and classifies each planned diff into:
250
+
251
+ * **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.
252
+ * **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.
253
+ * **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.
254
+ * **Category D (benign)** — diff adds template-resident lines absent from the downstream file. Standard template-forward drift; no hazard.
255
+
256
+ 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.
257
+
258
+ 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.
259
+
260
+ Until either is implemented, opt-out hygiene is a manual diff-review burden on every sync run.
179
261
 
180
262
 
181
263
  ==== `cimas diff`
@@ -265,6 +347,233 @@ NOTE: This sub-command is considered "`destructive`" as it
265
347
  alters the state of GitHub repositories by creating
266
348
  pull requests.
267
349
 
350
+ ===== `--supersede-stale` flag (Gap 4, implemented 2026-06-30 in cheaper form)
351
+
352
+ 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.
353
+
354
+ The `--supersede-stale` flag (opt-in) implements the cheaper-variant design discussed in https://github.com/metanorma/ci/issues/300#issuecomment-4832465285[`metanorma/ci#300` (Gap 4)]. For each target repo, when set:
355
+
356
+ 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).
357
+ 2. Prepends a `_Supersedes #X, #Y from prior cimas-sync waves._` line to the new PR's body, naming the superseded PRs.
358
+ 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.
359
+ 4. **Does NOT auto-close** the superseded PRs — the reviewer keeps authority over the close decision.
360
+
361
+ [source,sh]
362
+ ----
363
+ cimas open-prs --supersede-stale \
364
+ -r ~/src/cimas-wd-2026-06-29 \
365
+ -f cimas.yml \
366
+ -b cimas-sync-2026-06-29 \
367
+ -m "Cimas sync 2026-06-29: ..." \
368
+ --body-file /tmp/wave-body.md \
369
+ -g processor
370
+ ----
371
+
372
+ 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.
373
+
374
+ 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.
375
+
376
+ ===== `--flatten-stale` flag (Gap 4 full, implemented 2026-07-04)
377
+
378
+ `--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).
379
+
380
+ 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.
381
+
382
+ Behavioural delta vs `--supersede-stale`:
383
+
384
+ - Superseded PRs get labelled `superseded-closed-by-#N` (instead of `superseded-by-#N`).
385
+ - The comment on each superseded PR names auto-closure and points at the new PR.
386
+ - Each superseded PR is closed via `github_client.close_pull_request`.
387
+
388
+ [source,sh]
389
+ ----
390
+ cimas open-prs --flatten-stale \
391
+ -r ~/src/cimas-wd-2026-06-29 \
392
+ -f cimas.yml \
393
+ -b cimas-sync-2026-06-29 \
394
+ -m "Cimas sync 2026-06-29: ..." \
395
+ --body-file /tmp/wave-body.md \
396
+ -g processor
397
+ ----
398
+
399
+ 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.
400
+
401
+ ==== `cimas for-each`
402
+
403
+ The `for-each` sub-command executes an arbitrary shell command in each
404
+ repo's working copy.
405
+
406
+ Each repo's command exit status is reported per-repo (`[ERROR] '<cmd>'
407
+ failed in <repo> (exit N)`); if any command failed, `for-each` lists
408
+ the failing repos at the end and exits non-zero, so CI can detect it.
409
+
410
+ There is no strict requirement to use this command during CI
411
+ configuration update. You can use it on demand, for example if some
412
+ CI configuration files were removed
413
+
414
+ [source,sh]
415
+ ----
416
+ cimas sync -f {cimas-config-file} -r {cimas-working-area} \
417
+ -d {cimas-master-config-dir}
418
+
419
+ # e.g.
420
+ # cimas for-each -f cimas.yml -r ~/src/cimas-wd \
421
+ # -c "git rm .github/workflows/windows.yml"
422
+ ----
423
+
424
+
425
+ ==== End-to-end workflow: drift audit → preview → push
426
+
427
+ The full sequence for a "`sync + push a wave of drift corrections`"
428
+ operation is `setup` → `sync` → `diff` → `push`. Each sub-command has
429
+ its own required flags, so the full sequence is worth banking in one
430
+ place.
431
+
432
+ *Prerequisites for this workflow:*
433
+
434
+ * `GITHUB_TOKEN` exported (see prerequisites above).
435
+ * A checkout of the CI configuration master directory (e.g.
436
+ `metanorma/ci` for Metanorma). Below assume it lives at
437
+ `../ci/cimas-config/`.
438
+ * A fresh work-dir (e.g. `../cimas-wd-YYYY-MM-DD`).
439
+
440
+ *Step 1 — create the work-dir.*
441
+
442
+ [source,sh]
443
+ ----
444
+ mkdir ../cimas-wd-2026-07-22
445
+ ----
446
+
447
+ *Step 2 — `setup`: clone every mapped repo into the work-dir.*
448
+
449
+ [source,sh]
450
+ ----
451
+ cimas setup \
452
+ -r ../cimas-wd-2026-07-22 \
453
+ -f ../ci/cimas-config/cimas.yml
454
+ ----
455
+
456
+ Setup does not need `-d` (it only clones; it doesn't apply templates).
457
+
458
+ *Step 3 — `sync`: apply templates to each cloned repo locally.*
459
+
460
+ [source,sh]
461
+ ----
462
+ cimas sync \
463
+ -r ../cimas-wd-2026-07-22 \
464
+ -f ../ci/cimas-config/cimas.yml \
465
+ -d ../ci/cimas-config
466
+ ----
467
+
468
+ Both `-r` (work-dir), `-f` (cimas.yml), and `-d` (master path) are
469
+ required for `sync`. Missing `-d` produces
470
+ `[ERROR] config_master_path not set, aborting.`
471
+
472
+ *Step 4 — `cimas diff`: preview the aggregated diff. This is the primary
473
+ preview mechanism.*
474
+
475
+ [source,sh]
476
+ ----
477
+ cimas diff \
478
+ -r ../cimas-wd-2026-07-22 \
479
+ -f ../ci/cimas-config/cimas.yml \
480
+ -d ../ci/cimas-config
481
+ ----
482
+
483
+ Same three flags as `sync`. Prints `git diff` output per drifted repo.
484
+ This is the decision surface — read it and decide which classes to
485
+ propagate, and which should become opt-out entries in `cimas.yml`.
486
+ Repos with no drift show no diff at all.
487
+
488
+ *Quick count of repos with actual drift (post-sync, pre-push):*
489
+
490
+ [source,sh]
491
+ ----
492
+ cd ../cimas-wd-2026-07-22 && for d in */; do
493
+ [ -n "$(git -C "$d" status --porcelain 2>/dev/null)" ] && echo "${d%/}"
494
+ done | tee /tmp/drifted-repos.txt | wc -l
495
+ ----
496
+
497
+ Useful as a pre-push census: `cimas push` skips no-drift repos with a
498
+ "Skipping no-op push" line, so this one-liner gives you the drifted
499
+ subset up front, before any branch is provisioned.
500
+
501
+ [NOTE]
502
+ ====
503
+ The `-b` and `-m` push options are both required and validated up
504
+ front — `cimas push` exits 1 before touching any repo if either is
505
+ missing. A `cimas diff` preview needs neither.
506
+ ====
507
+
508
+ *Step 5 — real push (only after diff review).*
509
+
510
+ [source,sh]
511
+ ----
512
+ cimas push \
513
+ -r ../cimas-wd-2026-07-22 \
514
+ -f ../ci/cimas-config/cimas.yml \
515
+ -b cimas-sync-2026-07-22 \
516
+ -m "cimas: sync <date> drift-audit findings"
517
+ ----
518
+
519
+ Creates `cimas-sync-2026-07-22` on each mapped repo and pushes it. Does
520
+ NOT open PRs — that's `cimas open-prs` as a separate step.
521
+
522
+ [IMPORTANT]
523
+ ====
524
+ `cimas push` **skips repos with no drift**: each is logged as
525
+ `Skipping no-op push to <repo> (no drift)` and no branch is
526
+ provisioned for it. The push summary separates `Pushed with drift`
527
+ from `Skipped (no drift)`, so watchers only get notified for repos
528
+ that actually changed.
529
+
530
+ The next-stage `cimas open-prs` tolerates these skips: a repo whose
531
+ wave branch is absent on the remote is skipped with a WARNING, and an
532
+ empty PR (`No commits between`) is skipped too.
533
+
534
+ `cimas push --dry-run` prints the actions it would take without
535
+ executing them; `cimas diff` remains the primary preview of what
536
+ actually changes.
537
+ ====
538
+
539
+ *Step 6 — open PRs (optional; only for the drift-having branches).*
540
+
541
+ See the `cimas open-prs` section above for the full flag list.
542
+
543
+ .Required-flag matrix per sub-command
544
+ [cols="1,3", options="header"]
545
+ |===
546
+ | Sub-command | Required flags
547
+
548
+ | `setup` | `-r` `-f`
549
+ | `sync` | `-r` `-f` `-d`
550
+ | `diff` | `-r` `-f`
551
+ | `pull` | `-r` `-f`
552
+ | `push` | `-r` `-f` `-b` `-m` `-g`
553
+ | `open-prs` | `-r` `-f` `-b` (merge branch) `-m` (PR title) `-g`
554
+ | `for-each` | `-r` `-f` `-c` `-g`
555
+ | `cleanup-merged-prs` | `-r` `-f` `-b` `-g`
556
+ | `cleanup-closed-prs` | `-r` `-f` `-g`
557
+ | `cleanup-orphan-files` | `-r` `-f` `-g` (plus `-b` `-m` under `--push-after`)
558
+ | `release-preflight` | `-r` `-f` `--repo`
559
+ |===
560
+
561
+ Missing required flags fail fast with
562
+ `Error: <command>: missing required option(s): ...` and exit 1 before
563
+ any repo is touched. `-g` is required by the scope guard on every
564
+ remote-mutating sub-command (see the warning above).
565
+
566
+ .Gotchas summary
567
+ [cols="1,3", options="header"]
568
+ |===
569
+ | Gotcha | Symptom
570
+ | Missing `-d` on `sync` | `[ERROR] config_master_path not set, aborting.`
571
+ | Missing `-b` on `push`/`open-prs` | `Error: Missing -b/--push-branch value`
572
+ | Missing `GITHUB_TOKEN` | `[WARNING] Visibility fetch failed ...; defaulting to \`private\` (safer).` — sync is inaccurate for public repos.
573
+ | 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 ...`).
574
+ | Config without a `repositories:` section | Error at startup: `no `repositories:` section in ... — nothing to operate on`.
575
+ |===
576
+
268
577
 
269
578
  == Configuration
270
579
 
@@ -401,6 +710,82 @@ repositories:
401
710
  .github/workflows/windows.yml: gh-actions/model/windows.yml
402
711
  ----
403
712
 
713
+ ==== Proposed schema extension — per-repo `with:` rendering (Gap 1, not yet implemented)
714
+
715
+ NOTE: This subsection documents a **proposed** extension to the `files:` schema, not currently implemented. See https://github.com/metanorma/ci/issues/300[`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`).
716
+
717
+ 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).
718
+
719
+ 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:
720
+
721
+ [source,yaml]
722
+ ----
723
+ metanorma:
724
+ remote: ssh://git@github.com/metanorma/metanorma
725
+ branch: main
726
+ files:
727
+ # Existing string form — kept verbatim, no per-repo with:
728
+ .rubocop.yml: gh-actions/master/.rubocop.yml
729
+ .github/workflows/release.yml: gh-actions/master/release.yml
730
+
731
+ # Proposed map form — template + per-repo with: values
732
+ .github/workflows/rake.yml:
733
+ template: gh-actions/master/rake.yml
734
+ with:
735
+ private-fonts: true
736
+ ----
737
+
738
+ 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:
739
+
740
+ [source,yaml]
741
+ ----
742
+ jobs:
743
+ rake:
744
+ uses: metanorma/ci/.github/workflows/generic-rake.yml@main
745
+ <% if defined?(file_with) && file_with.any? -%>
746
+ with:
747
+ <% file_with.each do |k, v| -%>
748
+ <%= k %>: <%= v.inspect %>
749
+ <% end -%>
750
+ <% end -%>
751
+ secrets:
752
+ pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
753
+ ----
754
+
755
+ 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.
756
+
757
+ ==== Proposed schema extension — monorepo sub-template family (Gap 2, not yet implemented)
758
+
759
+ NOTE: This subsection documents a **proposed** extension to support monorepo-shaped repositories, not currently implemented. See https://github.com/metanorma/ci/issues/300[`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.
760
+
761
+ 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.
762
+
763
+ Concrete example: `metanorma/pubid` is a monorepo of `pubid-*` gems under `gems/`. Its `.github/workflows/rake.yml` has been forked structurally:
764
+
765
+ [source,yaml]
766
+ ----
767
+ jobs:
768
+ rake:
769
+ uses: ./.github/workflows/generic-rake.yml # local path, not metanorma/ci
770
+ with:
771
+ monorepo: true
772
+ gem_directory: gems
773
+ secrets:
774
+ pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
775
+ ----
776
+
777
+ 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.
778
+
779
+ The proposed remediation pairs a new template family with reusable-workflow extensions:
780
+
781
+ . 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.
782
+ . 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.
783
+ . Monorepo repos' `cimas.yml` entries reference the new template family and (per Gap 1's proposed schema extension) declare the per-monorepo `with:` values.
784
+
785
+ 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.
786
+
787
+ Until this lands, monorepo repos remain outside cimas-sync's scope and maintain their CI configuration by hand.
788
+
404
789
  === `groups`
405
790
 
406
791
  Cimas offers "`grouping`" functionality to allow you to work with groups
@@ -447,11 +832,79 @@ groups:
447
832
  ----
448
833
 
449
834
 
835
+ === Patches
836
+
837
+ In addition to full-file sync via `files:`, cimas supports in-place
838
+ line-level edits via a top-level `patches:` section. A patch is a named
839
+ regex find/replace that is applied to files already present in each target
840
+ repo. Patches are typically used for values (like a minimum Ruby version)
841
+ that live inside per-repo unique files (like `.gemspec`s) and therefore
842
+ cannot be copied wholesale.
843
+
844
+ [source,yaml]
845
+ ----
846
+ patches:
847
+ ruby_version:
848
+ files:
849
+ - "*.gemspec"
850
+ find: 'spec\.required_ruby_version\s*=.*'
851
+ replace: 'spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")'
852
+ groups: [processor, pubid, model]
853
+ ----
854
+
855
+ Each patch entry supports these keys:
856
+
857
+ * `files` — array of glob patterns, evaluated from each target repo's
858
+ root. Use `**/*.ext` for recursive matching.
859
+ * `find` — Ruby regex (as a string). Compiled with `Regexp.new`.
860
+ * `replace` — replacement string passed to `gsub`. Backreferences (`\\1`,
861
+ etc.) work.
862
+ * `groups` — which `groups:` (defined elsewhere in the config) the patch
863
+ applies to. A repo receives the patch if it appears in any listed group.
864
+
865
+ Patches are applied during `cimas sync`, immediately after the file-copy
866
+ phase, and the resulting changes are staged via `git add` so they flow
867
+ through `cimas push` and `cimas open-prs` the same way as file-copy
868
+ changes.
869
+
870
+ If a patch's `files` glob matches nothing, or the `find` regex matches no
871
+ content in a file, cimas logs a warning but does not fail — this makes it
872
+ safe to leave stale patches in config without blocking other repos.
873
+
874
+ Unlike file-copy sync, patches do **not** prepend the
875
+ "Auto-generated by Cimas" header, since they modify existing files in
876
+ place rather than replacing them.
877
+
450
878
 
451
879
  == Development
452
880
 
453
881
  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.
454
882
 
883
+ The test suite is fully offline: git-touching specs run against real
884
+ repositories created in temp directories, with a local bare repository
885
+ as the push remote — no network and no GitHub access are needed. This
886
+ includes an end-to-end lifecycle spec (`sync` → `diff` → `push`).
887
+
888
+ === Architecture overview (for contributors)
889
+
890
+ * `exe/cimas` boots `Cimas::Cli::Runner` (Thor): the CLI surface —
891
+ option parsing, help (`cimas help [COMMAND]`), and one-line
892
+ delegation per sub-command.
893
+ * `Cimas::Cli::Command` is the orchestrator. One `COMMANDS` registry
894
+ entry per subcommand drives dispatch behavior: the remote-mutating
895
+ classification (scope guard + blast-radius announcement) and the
896
+ required options (fail-fast validation).
897
+ * Domain models are value objects over the cimas.yml sections:
898
+ `Cimas::Repository` (`repositories:`), `Cimas::Patch` (`patches:`).
899
+ * Seams: `Cimas::WorkingCopy` is the git seam (all git-gem knowledge
900
+ lives there — subcommands never call the git gem directly);
901
+ `Cimas::GitHub` is the Octokit boundary (client, remote→slug
902
+ mapping, visibility fallback); `Cimas::OrphanFiles` is the pure
903
+ orphan-detection logic; `Cimas::ReleasePreflight` is the extracted
904
+ release check-runner.
905
+ * `Cimas::GENERATED_HEADER` is the single definition of the
906
+ generated-file header shared by the writer and orphan detection.
907
+
455
908
 
456
909
  == Contributing
457
910
 
data/bin/console CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
- require "ci/master"
4
+ require "cimas"
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/cimas.gemspec CHANGED
@@ -1,7 +1,5 @@
1
-
2
1
  lib = File.expand_path("../lib", __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "cimas"
5
3
  require "cimas/version"
6
4
 
7
5
  Gem::Specification.new do |spec|
@@ -34,11 +32,12 @@ Gem::Specification.new do |spec|
34
32
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
33
  spec.require_paths = ["lib"]
36
34
 
37
- spec.add_dependency "travis"
35
+ spec.add_dependency "thor"
38
36
  spec.add_dependency "octokit"
39
37
  spec.add_dependency "git"
38
+ spec.add_dependency "ostruct"
40
39
 
41
- spec.add_development_dependency "bundler", "~> 2.0"
42
- spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "bundler", ">= 2.0"
41
+ spec.add_development_dependency "rake", "~> 13.0"
43
42
  spec.add_development_dependency "rspec", "~> 3.0"
44
43
  end