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.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +18 -0
- data/.github/workflows/release.yml +31 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +29 -0
- data/CLAUDE.md +56 -0
- data/README.adoc +467 -14
- data/bin/console +1 -1
- data/bin/rspec +29 -0
- data/cimas.gemspec +4 -5
- data/exe/cimas +9 -201
- data/lib/cimas/cli/command.rb +837 -245
- data/lib/cimas/cli/error.rb +8 -0
- data/lib/cimas/cli/runner.rb +239 -0
- data/lib/cimas/cli.rb +7 -0
- data/lib/cimas/github.rb +47 -0
- data/lib/cimas/orphan_files.rb +37 -0
- data/lib/cimas/patch.rb +25 -0
- data/lib/cimas/release_preflight.rb +140 -0
- data/lib/cimas/repository.rb +33 -0
- data/lib/cimas/version.rb +1 -1
- data/lib/cimas/working_copy.rb +136 -0
- data/lib/cimas.rb +29 -2
- data/plans/cimas-revival-and-release-workflow-realignment.md +2158 -0
- metadata +40 -12
- data/Gemfile.lock +0 -86
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://
|
|
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
|
|
83
|
-
https://github.com/metanorma/
|
|
84
|
-
|
|
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
|
-
* `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
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 "
|
|
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", "
|
|
42
|
-
spec.add_development_dependency "rake", "~>
|
|
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
|