veragem 0.1.0 → 0.2.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +230 -34
  3. data/Gemfile.lock +254 -0
  4. data/README.md +143 -48
  5. data/lib/veragem/citation.rb +116 -0
  6. data/lib/veragem/generator.rb +105 -56
  7. data/lib/veragem/license.rb +27 -0
  8. data/lib/veragem/name.rb +110 -48
  9. data/lib/veragem/options.rb +116 -30
  10. data/lib/veragem/project.rb +200 -39
  11. data/lib/veragem/questions.rb +163 -0
  12. data/lib/veragem/summary.rb +62 -0
  13. data/lib/veragem/version.rb +2 -1
  14. data/lib/veragem.rb +12 -3
  15. data/plugins.rb +4 -0
  16. data/sig/veragem.rbs +151 -23
  17. data/template/.gitattributes +3 -0
  18. data/template/.githooks/pre-commit +2 -3
  19. data/template/.github/PULL_REQUEST_TEMPLATE.md +2 -1
  20. data/template/.github/dependabot.yml +4 -8
  21. data/template/.github/image-registries.sh +4 -3
  22. data/template/.github/workflows/dependabot-automerge.yml +29 -34
  23. data/template/.github/workflows/dependency-review.yml +19 -15
  24. data/template/.github/workflows/image.yml +65 -14
  25. data/template/.github/workflows/links.yml +35 -0
  26. data/template/.github/workflows/nix.yml +3 -3
  27. data/template/.github/workflows/release.yml +135 -22
  28. data/template/.github/workflows/ruby-ci.yml +31 -9
  29. data/template/.github/workflows/security.yml +15 -9
  30. data/template/.github/workflows/style-check-cocogitto.yml +3 -4
  31. data/template/.github/workflows/style-check-dprint.yml +10 -10
  32. data/template/.gitignore +5 -0
  33. data/template/.mutineer.yml +28 -0
  34. data/template/.rubocop.yml +49 -10
  35. data/template/.simplecov.erb +10 -0
  36. data/template/.yard-lint.yml +386 -0
  37. data/template/.yardopts +2 -0
  38. data/template/AGENTS.md.erb +76 -38
  39. data/template/CITATION.cff.erb +50 -0
  40. data/template/CODE_OF_CONDUCT.md.erb +1 -1
  41. data/template/CONTRIBUTING.md.erb +107 -48
  42. data/template/Gemfile +10 -7
  43. data/template/README.md.erb +12 -12
  44. data/template/Rakefile +167 -31
  45. data/template/Steepfile.erb +1 -1
  46. data/template/bin/setup +2 -0
  47. data/template/cliff.toml +40 -23
  48. data/template/dprint.json +1 -6
  49. data/template/flake.lock +3 -3
  50. data/template/flake.nix +25 -10
  51. data/template/lib/%path%/cli.rb.erb +19 -15
  52. data/template/lib/%path%/version.rb.erb +1 -1
  53. data/template/lib/%path%.rb.erb +3 -1
  54. data/template/sig/%path%.rbs.erb +1 -1
  55. data/template/test/test_%underscored%.rb.erb +4 -3
  56. data/template/test/test_cli.rb.erb +37 -12
  57. data/template/test/test_helper.rb.erb +15 -7
  58. metadata +16 -4
  59. data/template/.github/workflows/rubocop-autocorrect.yml +0 -114
@@ -2,8 +2,6 @@
2
2
 
3
3
  Thank you for considering contributing to `<%= name %>`!
4
4
 
5
- A project just generated: start at [First steps](#first-steps), under Maintainers.
6
-
7
5
  ## Prerequisites
8
6
 
9
7
  Either **Nix** -- `nix develop` gives Ruby and every tool below at the versions `flake.lock` pins
@@ -12,7 +10,7 @@ CI (nixpkgs packages few of them, and older).
12
10
 
13
11
  - **[Ruby](https://www.ruby-lang.org) 4.0** or later, with Bundler (Ruby's own). `bin/setup`
14
12
  installs the gems `Gemfile.lock` pins: Rake, RuboCop, its extensions and rubydex (its project
15
- index), Steep, TypeProf, Minitest, SimpleCov, pbt, bundler-audit.
13
+ index), Steep, TypeProf, Minitest, SimpleCov, pbt, mutineer, YARD, yard-lint, bundler-audit, IRB.
16
14
  - **[ShellCheck](https://www.shellcheck.net/)** and **[shfmt](https://github.com/mvdan/sh)** --
17
15
  the shell's linter and formatter (its style is `.editorconfig`'s).
18
16
  - **[actionlint](https://github.com/rhysd/actionlint)** -- lints the workflows (and, through
@@ -20,10 +18,13 @@ CI (nixpkgs packages few of them, and older).
20
18
  - **[zizmor](https://docs.zizmor.sh/)** -- audits the workflows' security, and Dependabot's
21
19
  configuration.
22
20
  - **[dprint](https://dprint.dev/)** -- formats the Markdown and TOML.
23
- - **[cocogitto](https://docs.cog.tools/)** (`cog`) -- enforces Conventional Commits.
21
+ - **[cocogitto](https://docs.cocogitto.io/)** (`cog`) -- enforces Conventional Commits.
24
22
  - **[git-cliff](https://git-cliff.org/)** -- writes the changelog at a release.
23
+ - **[lychee](https://lychee.cli.rs/)** -- checks the links of the files git tracks but the
24
+ tests', online (`rake links`; `links.yml` monthly too).
25
25
  - **[Nix](https://nixos.org)** for the last of `rake checks`, the flake's checks (`rake nix`):
26
- without it, `rake checks` stops there, every other check passed.
26
+ without it, `rake checks` stops there, every other check passed. `rake format` needs it too, for
27
+ `nix fmt`, before shfmt and RuboCop.
27
28
 
28
29
  ## Setup
29
30
 
@@ -36,14 +37,15 @@ bin/setup
36
37
  ```
37
38
 
38
39
  3. Optionally, `bundle exec rake hooks`: `.githooks/` then runs RuboCop, the shell, the workflows
39
- and dprint before each commit, checks its message as a Conventional Commit, and every pushed
40
- commit's message before a push: what CI would refuse, minutes earlier.
40
+ and dprint before each commit, checks its message as a Conventional Commit, and each commit's
41
+ in a pushed ref's history before a push: what CI would refuse, minutes earlier.
41
42
  `git config --unset core.hooksPath` turns them off.
42
43
 
43
44
  ## Guidelines
44
45
 
45
- Before you start, read [AGENTS.md](AGENTS.md): the lint and type policy, and what to do when a
46
- cop or a diagnostic fires. It applies to human and AI-assisted contributions alike.
46
+ Before you start, read [AGENTS.md](AGENTS.md): the lint and type policy, what to do when a cop or a
47
+ diagnostic fires, the comments, the commits, and what a change passes before it is done. It applies
48
+ to human and AI-assisted contributions alike.
47
49
 
48
50
  ## Testing
49
51
 
@@ -53,8 +55,21 @@ cop or a diagnostic fires. It applies to human and AI-assisted contributions ali
53
55
  `Pbt.assert { Pbt.property(...) { ... } }`): it is checked on many generated inputs, and a
54
56
  failure is shrunk to its smallest counterexample, with the seed that reproduces it.
55
57
  - **Regressions:** if you are fixing a bug, include a test that reproduces the previous failure.
56
- - **Coverage:** every line, branch and method of `lib/` (SimpleCov's `strict` profile,
57
- `test/test_helper.rb`). Code a test cannot reach is questioned before it is excluded.
58
+ - **Coverage:** every line, branch and method of `lib/` (SimpleCov's `strict` profile, configured in
59
+ `.simplecov`, started by `rake test`). Code no test needs is deleted, not excluded (AGENTS.md,
60
+ §4). Some tests alone (`bundle exec rake test N=/version/`) fail the gate (exit 2) though they
61
+ pass: Minitest's verdict is printed above SimpleCov's. One file run alone is not measured, its
62
+ warnings raised as `rake test` raises them (the helper first, before the file is parsed):
63
+ `bundle exec ruby -w -Ilib -Itest -rtest_helper test/test_<%= underscored %>.rb`.
64
+ - **Warnings:** one from `lib/` or the tests fails them, every category of Ruby's on
65
+ (`test/test_helper.rb`); a gem's only prints.
66
+ - **Mutations:** `bundle exec rake mutation` changes `lib/` a mutant at a time (mutineer,
67
+ `.mutineer.yml`) and lists each one no test catches: a test missing, or a change that keeps the
68
+ behaviour, which `.mutineer.yml`'s `ignore:` holds with its reason. One survivor fails it
69
+ (`threshold: 100`). It takes minutes: `rake checks` mutates the lines changed since `origin/main`
70
+ alone (`rake mutation:changed`; all of them where there is none), and `ruby-ci.yml` every line.
71
+ Each first fails on an ignore that names no mutant (`rake mutation:ignores`): its method changed
72
+ or moved, so it hides nothing.
58
73
 
59
74
  ## Development Workflow
60
75
 
@@ -64,14 +79,18 @@ cop or a diagnostic fires. It applies to human and AI-assisted contributions ali
64
79
  2. Run the checks. The Rakefile runs the workflows' commands:
65
80
 
66
81
  ```sh
67
- bundle exec rake # RuboCop, Steep, the tests: ruby-ci.yml
68
- bundle exec rake checks # and bundler-audit, dprint, cog, the shell, the workflows, the flake's checks
69
- bundle exec rake format # RuboCop's corrections, dprint and nixfmt, in place
70
- bundle exec rake typeprof # the signatures TypeProf infers from lib/, to start sig/ from
71
- nix build # the gem, built and installed by Nix (flake.nix)
82
+ bundle exec rake # RuboCop, Steep, the tests, yard-lint: ruby-ci.yml
83
+ bundle exec rake checks # and bundler-audit, dprint, cog, the shell, the workflows, the
84
+ # links, the changed lines' mutations, the flake's checks
85
+ bundle exec rake rubocop:changed # RuboCop on the files changed since origin/main
86
+ bundle exec rake format # dprint, nixfmt, shfmt and RuboCop's corrections, in place
87
+ bundle exec rake typeprof # the signatures TypeProf infers from lib/, to start sig/ from
88
+ bundle exec rake yard # the documentation of lib/, in doc/
89
+ bundle exec rake graph # its classes and modules, for Graphviz: | dot -Tsvg -o graph.svg
90
+ nix build # the gem, built and installed by Nix (flake.nix)
72
91
  ```
73
92
 
74
- `bundle exec rake -T` lists every task.
93
+ `bundle exec rake -T` lists every task, and `bundle exec rake -D` describes each in full.
75
94
 
76
95
  A new dependency of the gem goes in the gemspec (`spec.add_dependency`), and, for the Nix package,
77
96
  its nixpkgs package in flake.nix's `dependencies` (`pkgs.rubyPackages_4_0.<gem>`): the flake's
@@ -81,7 +100,8 @@ new tool goes in the Gemfile, which the built gem never holds: its dependencies
81
100
  alone. `bundle install` then updates the lock. Steep reads a library's signatures when the
82
101
  Steepfile names it (`library "json"`): the standard library's, and a gem's own (its `sig/`). A gem
83
102
  that ships none may have them in gem_rbs_collection, read through an RBS collection
84
- (`rbs collection init`, then `rbs collection install`, as rbs's `docs/collection.md` describes).
103
+ (`rbs collection init`, then `rbs collection install`, as rbs's `docs/collection.md` describes;
104
+ `steep check` installs it from the committed lock where it is missing, as in CI).
85
105
 
86
106
  ## Creating a Pull Request
87
107
 
@@ -115,24 +135,45 @@ Then, in whichever order suits you:
115
135
  `.github/workflows/dependabot-automerge.yml`: without required status checks, it merges bumps
116
136
  that CI never gated.
117
137
  - Enable private vulnerability reporting (Settings → Advanced Security; a public repository's
118
- alone): it is off by default, and [SECURITY.md](SECURITY.md) and the issue form point to it.
138
+ alone): it is off by default, and [SECURITY.md](SECURITY.md) and the issue chooser's link point
139
+ to it.
119
140
  - Enable the dependency graph (Settings → Advanced Security), if it is off: the dependency review
120
141
  (`dependency-review.yml`) fails without it. A private repository's needs GitHub Code Security
121
142
  too, an organization's; without it, delete the workflow.
122
- - Add a pending trusted publisher on [RubyGems](https://rubygems.org/profile/oidc/pending_trusted_publishers)
123
- for the gem `<%= name %>`: this repository, the workflow `release.yml`, the environment
124
- `release`. A release tag then publishes without a stored key, its first version too, and its
125
- GitHub release follows RubyGems': without the publisher, neither is made.
126
- - If you limit the `release` environment's deployments (Settings → Environments), admit the
127
- release tags, a rule of ref type Tag, `v*`: the RubyGems job runs on the tag, and an environment
128
- that refuses it publishes nothing.
143
+ - Add a pending trusted publisher on
144
+ [RubyGems](https://rubygems.org/profile/oidc/pending_trusted_publishers) for the gem
145
+ `<%= name %>`: this repository, the workflow `release.yml`, the environment `release` (a
146
+ publisher without one admits any of the workflow's jobs, its signing job's too). A release tag
147
+ then publishes without a stored key, its first version too, and its GitHub release follows
148
+ RubyGems': without the publisher, neither is made.
149
+ - Limit the `release` environment's deployments to the release tags (Settings → Environments, a
150
+ rule of ref type Tag, `v*`): a trusted publisher names the environment, not a ref, so without the
151
+ rule a workflow pushed to any branch could publish the gem. And limit who creates the release tags
152
+ (Settings → Rules → Rulesets, a tag ruleset on `v*`): a tag pushed on any commit runs that
153
+ commit's workflows in the environment.
129
154
  <%- if exe -%>
130
155
  - After the first release, make the image public: GHCR publishes a new package as private (its
131
156
  page, Package settings → Change visibility).
157
+ - Limit the `image` environment's deployments to the release tags as well (a rule of ref type Tag,
158
+ `v*`): its jobs push the image.
132
159
  - To publish the image to Docker Hub too: create its repository there, then set the repository
133
- variable `DOCKERHUB_USERNAME` and the secret `DOCKERHUB_TOKEN` (an access token that writes) --
134
- and the variable `DOCKERHUB_REPOSITORY` (`namespace/name`) unless it is `<username>/<%= name %>`.
135
- A Docker Hub failure in the build (its login, its name, a push) holds GHCR's tag back too.
160
+ variable `DOCKERHUB_USERNAME` and the `image` environment's secret `DOCKERHUB_TOKEN` (an access
161
+ token that writes; only the environment's jobs, on a release tag, can read it) -- and the variable
162
+ `DOCKERHUB_REPOSITORY` (`namespace/name`) unless it is `<username>/<%= name %>`. A Docker Hub
163
+ failure in a push (its login, its name) holds GHCR's tag back too.
164
+ <%- end -%>
165
+ <%- if citation -%>
166
+ - To archive each release with a DOI, enable the repository on [Zenodo](https://zenodo.org) (its
167
+ profile menu → GitHub, the repository's toggle): a GitHub release, `release.yml`'s, is then
168
+ archived, its metadata `CITATION.cff`'s. After the first, uncomment the DOIs at the end of
169
+ `CITATION.cff` and set them: the concept DOI, which names every version (Zenodo's "Cite all
170
+ versions") and which GitHub's citation then links, and a release's own, which Zenodo mints only
171
+ after its tag, so the file can name an earlier version's alone.
172
+ <%- unless licenses.one? -%>
173
+ Zenodo's import refuses `CITATION.cff`'s list of licences, the Citation File Format's OR, so
174
+ it archives no release until
175
+ [zenodo/zenodo-rdm#1509](https://github.com/zenodo/zenodo-rdm/issues/1509) is fixed.
176
+ <%- end -%>
136
177
  <%- end -%>
137
178
  - Run `bundle exec rake checks` once to confirm your tools are complete: a step that stops on a
138
179
  missing command names it ([Prerequisites](#prerequisites) lists them; `nix develop` has them
@@ -152,32 +193,50 @@ bundle exec rake "bump[minor]" # or major, or patch
152
193
  bundle exec rake release
153
194
  ```
154
195
 
155
- `rake bump` refuses a release with nothing in it -- the commits since the last tag must hold a
156
- change a user meets (`feat`, `fix`, `perf`, `build`...): `chore`, `ci` and `style` alone make
157
- none. It sets the version in `lib/<%= path %>/version.rb` (the first release keeps the version
158
- declared, whatever the level), updates the lock, writes the changelog (git-cliff) and commits.
196
+ `rake bump` refuses a release with nothing in it: no commit since the last tag, merges and fixups
197
+ aside, which the changelog leaves out. It refuses a shallow clone, a clone missing the tags
198
+ (`git fetch --tags`), a version bumped but not yet released, and one already tagged. It sets the
199
+ version in `lib/<%= path %>/version.rb` (the first release keeps the version declared, whatever the
200
+ level), updates the lock, writes the changelog (git-cliff) and commits.
201
+ <%- if citation -%>
202
+ It sets `CITATION.cff`'s version and date-released too: a citation names the release.
203
+ <%- end -%>
159
204
  `rake release`, Bundler's own, builds the gem, tags `v<version>` and pushes the branch and the
160
- tag; it pushes no gem. The tag runs `release.yml`: the checks and the build, the push to RubyGems
161
- by Trusted Publishing, then the GitHub release of the same gem, its notes the changelog's new
162
- section.
205
+ tag; it pushes no gem. The tag runs `release.yml`: the Ruby checks (`rake`), then the build
206
+ (`gem build`, no bundle loaded) and its Sigstore attestation, the push to RubyGems by Trusted
207
+ Publishing, which shows the gem's provenance, then the GitHub release of the same gem, its notes
208
+ the changelog's new section.
163
209
  <%- if exe -%>
164
210
  Beside `release.yml`, waiting for no RubyGems push, `image.yml` publishes the command's OCI image
165
211
  to GHCR (`nix build .#image` builds it here).
166
212
  <%- end -%>
167
213
 
168
- `rake release` pushes to `main` directly: with "Require a pull request before merging" on, the
169
- maintainer cutting it needs the rule's bypass. A refused push leaves the release commit and
170
- removes the tag (Bundler's doing): run `rake release` again once allowed.
214
+ Should a run fail, re-run its failed jobs, not all of them: those keep the build's gem and
215
+ digests, and RubyGems refuses a version pushed twice. If the gem reached RubyGems but its job
216
+ failed anyway, the release job cannot run: make the GitHub release as it would, from the tag, with
217
+ `--latest=false` unless it is the highest release and `--prerelease` for a pre-release:
218
+
219
+ ```sh
220
+ git checkout vVERSION
221
+ gem fetch <%= name %> --version VERSION
222
+ .github/release-notes.sh CHANGELOG.md VERSION > notes.md
223
+ gh release create vVERSION <%= name %>-VERSION.gem --verify-tag --title vVERSION --notes-file notes.md
224
+ ```
225
+
226
+ `rake release` pushes to `main` directly, and a `v*` tag: with "Require a pull request before
227
+ merging" on, the maintainer cutting it needs the rule's bypass, and the tag ruleset's leave to
228
+ create the tag. A refused push leaves the release commit and removes the tag (Bundler's doing): run
229
+ `rake release` again once allowed. An interrupted push (Ctrl-C) can keep the tag, and a second
230
+ `rake release` then pushes nothing: delete it first (`git tag -d v<version>`).
171
231
 
172
232
  ## Updating the tools
173
233
 
174
- Dependabot moves the gems (`Gemfile.lock`) and the actions weekly, and the flake's inputs
175
- monthly; `dependabot-automerge.yml` merges the minor and patch bumps once the checks pass.
234
+ Dependabot moves the gems (`Gemfile.lock`) and the actions weekly, and the flake's inputs monthly;
235
+ `dependabot-automerge.yml` merges the minor and patch bumps once the checks pass. An advisory with
236
+ no fixed version yet fails `security.yml` on every pull request until you weigh it: its ID under
237
+ `ignore:` in `.bundler-audit.yml` (bundler-audit's own file, created then) lets the checks pass.
176
238
  dprint's plugins, pinned in `dprint.json`, move by hand: `dprint config update`. A new RuboCop
177
- brings new cops (`NewCops: enable`), a new Steep new diagnostics: read them before silencing any.
178
- A bump the new cops fail gets RuboCop's safe corrections as a commit on its branch
179
- (`rubocop-autocorrect.yml`). GitHub holds the workflows of a commit a workflow's token pushes
180
- until you approve them (the pull request's "Approve workflows to run"); corrected, the bump then
181
- merges as any bump. What safe corrections leave is yours to fix, in a pull request of yours too: a
182
- failed run's summary
183
- holds the corrections as a patch, to `git apply`.
239
+ brings new cops (`NewCops: enable`), a new Steep new diagnostics: read them before silencing any. A
240
+ bump the new cops fail stays unmerged: its failed run's summary holds RuboCop's safe corrections as
241
+ a patch, to `git apply` on its branch (no workflow pushes code the bumped gems wrote); what they
242
+ leave is yours to fix.
data/template/Gemfile CHANGED
@@ -2,23 +2,26 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # The gem's own dependencies are its gemspec's.
6
5
  gemspec
7
6
 
8
- # The development tools, unconstrained: Gemfile.lock pins them and Dependabot moves the lock. No
9
- # group needed: the built gem holds neither this file nor the lock, its dependencies the
10
- # gemspec's alone.
11
7
  gem "bundler-audit"
12
8
  gem "irb"
9
+ gem "rake"
10
+
13
11
  gem "minitest"
12
+ gem "mutineer"
14
13
  gem "pbt"
15
- gem "rake"
14
+ gem "simplecov"
15
+
16
16
  gem "rubocop"
17
17
  gem "rubocop-minitest"
18
18
  gem "rubocop-packaging"
19
19
  gem "rubocop-performance"
20
20
  gem "rubocop-rake"
21
- gem "rubydex"
22
- gem "simplecov"
21
+ gem "rubydex" # for rubocop UseProjectIndex
22
+
23
+ gem "yard"
24
+ gem "yard-lint"
25
+
23
26
  gem "steep"
24
27
  gem "typeprof"
@@ -1,6 +1,6 @@
1
1
  # <%= name %>
2
2
 
3
- <%= summary %>
3
+ <%= markdown(summary) %>
4
4
 
5
5
  ## Requirements
6
6
 
@@ -17,9 +17,10 @@ bundle add <%= name %>
17
17
  ```
18
18
  <%- if exe -%>
19
19
 
20
- With Nix, `nix run github:<%= github %>/<%= name %>`. As an OCI image, for Linux on amd64 and arm64:
20
+ With Nix, or as an OCI image for Linux on amd64 and arm64:
21
21
 
22
22
  ```sh
23
+ nix run github:<%= github %>/<%= name %> -- --version
23
24
  docker run --rm ghcr.io/<%= github.downcase %>/<%= name %> --version
24
25
  ```
25
26
  <%- end -%>
@@ -42,18 +43,17 @@ require "<%= path %>"
42
43
 
43
44
  ## Development
44
45
 
45
- `bin/setup` installs the gems at `Gemfile.lock`'s versions (its first run writes the lock); then
46
- `bundle exec rake` runs the Ruby checks (RuboCop, Steep and the tests with their coverage gate)
47
- and `bundle exec rake checks` everything CI runs:
46
+ `bin/setup` installs the gems at `Gemfile.lock`'s versions. `bundle exec rake` runs the Ruby checks
47
+ (RuboCop, Steep, the tests and their coverage gate, yard-lint); `bundle exec rake checks` runs all
48
+ of CI's, the links too, mutating the changed lines alone:
48
49
 
49
50
  ```sh
50
51
  bin/setup
51
52
  bundle exec rake checks
52
53
  ```
53
54
 
54
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the tools those checks need, how a release is cut and,
55
- in a project just generated, its [first steps](CONTRIBUTING.md#first-steps); and
56
- [AGENTS.md](AGENTS.md) for the lint and type policy.
55
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the tools those checks need and how a release is cut,
56
+ and [AGENTS.md](AGENTS.md) for the rules a change follows: lint and types, comments, commits.
57
57
 
58
58
  ## License
59
59
  <%- if license == "dual" -%>
@@ -61,8 +61,8 @@ in a project just generated, its [first steps](CONTRIBUTING.md#first-steps); and
61
61
  Licensed under either of
62
62
 
63
63
  - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
64
- <http://www.apache.org/licenses/LICENSE-2.0>)
65
- - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
64
+ <https://www.apache.org/licenses/LICENSE-2.0>)
65
+ - MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/license/MIT>)
66
66
 
67
67
  at your option.
68
68
 
@@ -74,9 +74,9 @@ any additional terms or conditions.
74
74
  <%- elsif license == "mit" -%>
75
75
 
76
76
  Licensed under the MIT license ([LICENSE-MIT](LICENSE-MIT) or
77
- <http://opensource.org/licenses/MIT>).
77
+ <https://opensource.org/license/MIT>).
78
78
  <%- else -%>
79
79
 
80
80
  Licensed under the Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
81
- <http://www.apache.org/licenses/LICENSE-2.0>).
81
+ <https://www.apache.org/licenses/LICENSE-2.0>).
82
82
  <%- end -%>
data/template/Rakefile CHANGED
@@ -1,30 +1,123 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # `rake` runs the Ruby checks, as ruby-ci.yml does; `rake checks` runs everything CI runs.
3
+ # `rake` runs the Ruby checks, as ruby-ci.yml does; `rake checks` runs CI's checks, the links too
4
+ # (links.yml's, monthly), but mutates the changed lines alone (ruby-ci.yml: every line).
4
5
  # `rake -T` lists the tasks.
5
6
 
6
7
  require "bundler/audit/task"
7
8
  require "bundler/gem_tasks"
9
+ require "csv"
8
10
  require "json"
9
11
  require "minitest/test_task"
10
12
  require "rubocop/rake_task"
11
13
  require "steep/rake_task"
14
+ require "yard"
12
15
 
13
- Minitest::TestTask.create
16
+ # Coverage, `rake test`'s alone: started in the tests' process before they load lib/ (.simplecov);
17
+ # then the helper, whatever order the test files load in (shuffled): one loaded before it would only
18
+ # print its parse-time warnings
19
+ TEST_TASK = Minitest::TestTask.create do
20
+ self.test_prelude = %(require "simplecov"; SimpleCov.start; require "test_helper")
21
+ end
14
22
  RuboCop::RakeTask.new
15
23
  Steep::RakeTask.new
16
24
  Bundler::Audit::Task.new
17
25
 
18
- task default: %i[rubocop steep test]
26
+ # `rake yard`: lib/'s documentation, in doc/. Not sig/'s: YARD's RBS reader refuses a signature
27
+ # over several lines.
28
+ YARD::Rake::YardocTask.new { it.files = ["lib/**/*.rb"] }
29
+
30
+ task default: %i[rubocop steep steep:typed test yard_lint]
31
+
32
+ # A command's standard output, as UTF-8 whatever the locale (git-cliff's commits, their authors' names);
33
+ # its failure fails the task, as `sh`'s does.
34
+ def output(*command)
35
+ IO.popen(command, external_encoding: Encoding::UTF_8, &:read).tap do
36
+ raise "#{command.join(" ")} failed" unless Process.last_status.success?
37
+ end
38
+ end
19
39
 
20
- # TypeProf infers; Steep, against sig/, is the gate. TypeProf reads Ruby's core signatures alone,
21
- # not the standard library's.
22
- desc "Print the signatures TypeProf infers from lib/, to start sig/ from, and what it finds amiss"
40
+ # The files git tracks, those still on disk
41
+ def tracked = output("git", "ls-files", "-z").split("\0").select { File.file?(it) }
42
+
43
+ # git's shell scripts, by name or shebang (shfmt --find)
44
+ def scripts = output("shfmt", "--find", *tracked).lines(chomp: true)
45
+
46
+ # A call is untyped where its receiver is: steep check passes it, and a steep:ignore leaves it so.
47
+ # steep stats counts them, exiting 0 whatever it counts; a file it cannot type (status error, as a
48
+ # syntax error, which strict steep check reports as information) it counts as none.
49
+ desc "Check that Steep types each call of lib/. steep stats counts the untyped calls"
50
+ task "steep:typed" do
51
+ stats = CSV.parse(output("steep", "stats", "--format=csv"), headers: true)
52
+ failed = stats.filter_map do |row|
53
+ next "#{row["File"]}: not typed (#{row["Status"]})" unless row["Status"] == "success"
54
+
55
+ "#{row["File"]}: #{row["Untyped calls"]} untyped" unless row["Untyped calls"] == "0"
56
+ end
57
+ abort "error: calls Steep does not type:\n#{failed.join("\n")}" unless failed.empty?
58
+ end
59
+
60
+ # Steep, against sig/, is the gate. TypeProf reads Ruby's core signatures alone, not the standard
61
+ # library's.
62
+ desc "Print the signatures TypeProf infers. It reads lib/, to start sig/ from, and prints what it finds amiss"
23
63
  task :typeprof do
24
64
  sh "typeprof", "--show-errors", "lib"
25
65
  end
26
66
 
27
- desc "Update the advisory database, then check Gemfile.lock against it (bundler-audit)"
67
+ # yard graph draws what yardoc records (.yardoc): lib/'s, quietly, so the graph alone is printed, and
68
+ # its private methods too (no .yardopts, whose query keeps the public API)
69
+ desc "Print lib/'s classes and modules as a graph. yard graph writes it for Graphviz's `dot`"
70
+ task :graph do
71
+ sh "yard", "doc", "-n", "-q", "--no-stats", "--no-yardopts", "lib/**/*.rb"
72
+ sh "yard", "graph", "--protected", "--private", "--full", "--dependencies"
73
+ end
74
+
75
+ # Every test file `rake test` runs (its globs) but test_helper.rb, which every test requires and holds
76
+ # none: mutineer pairs lib/a/b.rb with one test file by its path (test/a/test_b.rb), and a test may
77
+ # reach any file.
78
+ def mutate(*options)
79
+ tests = FileList[*TEST_TASK.test_globs].exclude("test/test_helper.rb").uniq.flat_map { ["--test", it] }
80
+ sh "mutineer", "run", *options, *FileList["lib/**/*.rb"], *tests
81
+ end
82
+
83
+ # An ignore whose mutant is gone (its method changed or moved) hides nothing and goes stale: migrate
84
+ # --dry-run names each and fails, writing nothing; it counts an ignored copy mutineer drops as used.
85
+ desc "Check that each ignore of .mutineer.yml names a mutant of lib/. mutineer migrate --dry-run"
86
+ task "mutation:ignores" do
87
+ sh "mutineer", "migrate", "--dry-run", *FileList["lib/**/*.rb"]
88
+ end
89
+
90
+ desc "Mutate lib/ against the tests. A change none catches fails it (mutineer, .mutineer.yml)"
91
+ task(mutation: "mutation:ignores") { mutate }
92
+
93
+ # The commit the branch left origin/main at; nil where there is no origin/main (a gem never pushed)
94
+ def merge_base
95
+ base = IO.popen(%w[git merge-base HEAD origin/main], err: File::NULL, &:read).chomp
96
+ base unless base.empty?
97
+ end
98
+
99
+ # The lines changed since the merge base, uncommitted ones too, and every line of a file git does
100
+ # not track (a new one before `git add`), unless git ignores it; every line where there is none
101
+ desc "Mutate lib/'s lines changed since origin/main. `rake mutation` mutates all of them"
102
+ task "mutation:changed" => "mutation:ignores" do
103
+ base = merge_base
104
+ mutate(*(["--since", base, "--allow-empty"] if base))
105
+ end
106
+
107
+ # The files changed since the merge base, uncommitted and untracked ones too (`--changed`); every
108
+ # file where there is none
109
+ desc "Run RuboCop on the files changed since origin/main. `rake rubocop` checks them all"
110
+ task "rubocop:changed" do
111
+ base = merge_base
112
+ sh "rubocop", *("--changed=#{base}" if base)
113
+ end
114
+
115
+ desc "Lint lib/'s YARD documentation with yard-lint. Its rules: .yard-lint.yml"
116
+ task :yard_lint do
117
+ sh "yard-lint", "lib", "--stats"
118
+ end
119
+
120
+ desc "Update the advisories, check Gemfile.lock. bundler-audit fetches its database, then checks the lock"
28
121
  task audit: %w[bundle:audit:update bundle:audit:check]
29
122
 
30
123
  desc "Check the Markdown and TOML format (dprint)"
@@ -32,45 +125,43 @@ task :dprint do
32
125
  sh "dprint", "check"
33
126
  end
34
127
 
35
- desc "Check that every commit is a Conventional Commit (cocogitto)"
128
+ desc "Check every commit message with cocogitto. Each must be a Conventional Commit"
36
129
  task :cog do
37
130
  sh "cog", "check"
38
131
  end
39
132
 
40
- # A command's standard output; its failure fails the task, as `sh`'s does.
41
- def output(*command)
42
- IO.popen(command, &:read).tap { raise "#{command.join(" ")} failed" unless Process.last_status.success? }
43
- end
44
-
45
- desc "Lint and check the format of the shell scripts git tracks (shellcheck, shfmt)"
133
+ desc "Lint and format-check the shell scripts. It checks those git tracks, with shellcheck and shfmt"
46
134
  task :shell do
47
- tracked = output("git", "ls-files", "-z").split("\0").select { File.file?(it) }
48
- scripts = output("shfmt", "--find", *tracked).lines(chomp: true)
49
- sh "shellcheck", *scripts
50
- sh "shfmt", "--diff", *scripts
135
+ files = scripts
136
+ sh "shellcheck", *files
137
+ sh "shfmt", "--diff", *files
51
138
  end
52
139
 
53
- desc "Lint the workflows and audit their security (actionlint, zizmor)"
140
+ desc "Lint the workflows, audit their security. It runs actionlint and zizmor"
54
141
  task :workflows do
55
142
  sh "actionlint"
56
143
  sh "zizmor", "--offline", "--quiet", "--strict-collection", "."
57
144
  end
58
145
 
59
- desc "Run the flake's checks: the Nix format, the shell, the workflows, the package (git's files alone)"
146
+ desc "Run the flake's checks. They check the Nix format, the shell, the workflows and the package (git's files)"
60
147
  task :nix do
61
148
  sh "nix", "flake", "check"
62
149
  end
63
150
 
64
- desc "Run every check CI runs"
65
- task checks: %i[default audit dprint cog shell workflows nix]
151
+ desc "Run CI's checks, mutating the changed lines. It checks the links too; CI mutates every line"
152
+ task checks: %i[default audit dprint cog shell workflows links mutation:changed nix]
66
153
 
67
- desc "Format the Ruby, the Markdown and TOML, and the Nix (modifies files)"
68
- task format: "rubocop:autocorrect" do
154
+ # RuboCop last: it fails on an offense it cannot correct, after the others have formatted
155
+ desc "Format the Markdown, TOML, Nix, shell and Ruby files. It rewrites them in place"
156
+ task :format do
69
157
  sh "dprint", "fmt"
70
158
  sh "nix", "fmt"
159
+ sh "shfmt", "--write", *scripts
160
+ Rake::Task["rubocop:autocorrect"].invoke
71
161
  end
72
162
 
73
- desc "Use .githooks/: the quick checks before a commit, Conventional Commits for every message"
163
+ desc "Use .githooks/ for git's hooks. They run the quick checks before a commit, and check that every " \
164
+ "message is a Conventional Commit"
74
165
  task :hooks do
75
166
  sh "git", "config", "core.hooksPath", ".githooks"
76
167
  end
@@ -80,24 +171,67 @@ gemspec = Bundler::GemHelper.gemspec
80
171
  repository = gemspec.metadata.fetch("source_code_uri").delete_prefix("https://github.com/")
81
172
  cliff = ["git-cliff", "--offline", "--github-repo", repository]
82
173
 
174
+ # Online, a link that passed cached a day (.lycheecache); a refused connection or a 429 retried at
175
+ # least 10 s apart, so a brief outage is waited out. Not the tests' links, made up; nor GitHub's
176
+ # bare address, read off those the code builds; nor the gem's own pages, missing until it is pushed
177
+ # and published.
178
+ desc "Check the links in git's files with lychee. Not the tests', whose links are made up"
179
+ task :links do
180
+ files = tracked.grep_v(%r{\Atest/})
181
+ own = [gemspec.homepage, "https://rubygems.org/gems/#{gemspec.name}", "https://rubydoc.info/gems/#{gemspec.name}"]
182
+ skip = ["^#{Regexp.escape("https://github.com/")}$", "^(#{own.map { Regexp.escape(it) }.join("|")})($|/)"]
183
+ sh "lychee", "--no-progress", "--cache", "--retry-wait-time", "10", *skip.flat_map { ["--exclude", it] }, *files
184
+ end
185
+
83
186
  desc "Preview the changelog's next section"
84
187
  task :changelog do
85
188
  sh(*cliff, "--unreleased")
86
189
  end
87
190
 
191
+ # The release tags, refused where they do not tell the releases: a shallow clone, or a clone without
192
+ # them that has a CHANGELOG.md
193
+ def release_tags
194
+ if output("git", "rev-parse", "--is-shallow-repository") == "true\n"
195
+ abort "error: a shallow clone, which the changelog cannot be written from: git fetch --unshallow"
196
+ end
197
+ tags = output("git", "tag", "--list", "v[0-9]*").lines(chomp: true)
198
+ return tags unless tags.empty? && File.exist?("CHANGELOG.md")
199
+
200
+ abort "error: a CHANGELOG.md but no v* tag: git fetch --tags, or release the version bumped first"
201
+ end
202
+
88
203
  # The version to release: the declared one at the first release (no tag yet), else +level+'s
89
- # number bumped (0 major, 1 minor, 2 patch), those after it zeroed.
204
+ # number bumped (0 major, 1 minor, 2 patch), those after it zeroed. The current version must be
205
+ # tagged: else it was bumped and never released.
90
206
  def next_version(current, level)
91
207
  abort "error: #{current} is not MAJOR.MINOR.PATCH" unless current.match?(/\A\d+\.\d+\.\d+\z/)
92
- return current if output("git", "tag", "--list", "v[0-9]*").empty?
208
+ tags = release_tags
209
+ return current if tags.empty?
210
+
211
+ abort "error: v#{current} is not tagged: git fetch --tags, or release it first" unless tags.include?("v#{current}")
93
212
 
94
213
  parts = current.split(".").map(&:to_i)
95
214
  parts[level] += 1
96
215
  parts.fill(0, level + 1).join(".")
97
216
  end
98
217
 
99
- # The release commit: the version, the lock, the changelog.
100
- desc "Bump the version (major, minor or patch), write the changelog and commit"
218
+ # CITATION.cff, where the gem has one (bundle veragem --citation): its text with the release's version
219
+ # and date, or nil; read as UTF-8, as written, whatever the locale. The date is UTC's, as the
220
+ # changelog's (git-cliff's `date` filter).
221
+ def cited(version)
222
+ return unless File.exist?("CITATION.cff")
223
+
224
+ released = { "version" => version, "date-released" => Time.now.utc.strftime("%F") }
225
+ released.reduce(File.read("CITATION.cff", encoding: Encoding::UTF_8)) do |cff, (key, value)|
226
+ abort "error: CITATION.cff has no #{key}: line" unless cff.match?(/^#{key}: /)
227
+
228
+ cff.sub(/^#{key}: [^\r\n]*/, %(#{key}: "#{value}"))
229
+ end
230
+ end
231
+
232
+ # The release commit: the version, the lock, the changelog, and CITATION.cff's version and date.
233
+ desc "Bump the version, write the changelog, commit. The level is major, minor or patch; a first release " \
234
+ "keeps the declared version"
101
235
  task :bump, [:level] => "release:guard_clean" do |_, args|
102
236
  level = %w[major minor patch].index(args[:level]) or abort 'usage: rake "bump[major|minor|patch]"'
103
237
  current = gemspec.version.to_s
@@ -109,12 +243,14 @@ task :bump, [:level] => "release:guard_clean" do |_, args|
109
243
 
110
244
  file = "lib/#{gemspec.name.tr("-", "/")}/version.rb"
111
245
  abort "error: #{file} declares no \"#{current}\"" unless File.read(file).include?(%("#{current}"))
246
+ citation = cited(version)
112
247
  first = !File.exist?("CHANGELOG.md")
113
248
  begin
114
249
  File.write(file, File.read(file).sub(%("#{current}"), %("#{version}")))
250
+ File.write("CITATION.cff", citation) if citation
115
251
  sh "bundle", "lock"
116
252
  sh(*cliff, "--tag", "v#{version}", "--output", "CHANGELOG.md")
117
- sh "git", "add", file, "Gemfile.lock", "CHANGELOG.md"
253
+ sh "git", "add", file, "Gemfile.lock", "CHANGELOG.md", *("CITATION.cff" if citation)
118
254
  sh "git", "commit", "--message", "chore(release): v#{version}"
119
255
  rescue StandardError, Interrupt
120
256
  # The tracked files were committed (release:guard_clean): put back what this changed.
@@ -128,7 +264,7 @@ end
128
264
  # The gem is published by release.yml, on the tag, through RubyGems' Trusted Publishing:
129
265
  # `rake release` builds it, tags v<version> and pushes the branch and the tag, and pushes no gem.
130
266
  Rake::Task["release"].clear_comments
131
- Rake::Task["release"].add_description("Tag v#{gemspec.version}, push the branch and the tag: release.yml publishes it")
267
+ Rake::Task["release"].add_description("Tag v#{gemspec.version}, push the branch and the tag. release.yml publishes it")
132
268
  Rake::Task["release:rubygem_push"].clear_actions
133
269
  desc "Push no gem: release.yml publishes the tag"
134
270
  task "release:rubygem_push" do