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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +230 -34
- data/Gemfile.lock +254 -0
- data/README.md +143 -48
- data/lib/veragem/citation.rb +116 -0
- data/lib/veragem/generator.rb +105 -56
- data/lib/veragem/license.rb +27 -0
- data/lib/veragem/name.rb +110 -48
- data/lib/veragem/options.rb +116 -30
- data/lib/veragem/project.rb +200 -39
- data/lib/veragem/questions.rb +163 -0
- data/lib/veragem/summary.rb +62 -0
- data/lib/veragem/version.rb +2 -1
- data/lib/veragem.rb +12 -3
- data/plugins.rb +4 -0
- data/sig/veragem.rbs +151 -23
- data/template/.gitattributes +3 -0
- data/template/.githooks/pre-commit +2 -3
- data/template/.github/PULL_REQUEST_TEMPLATE.md +2 -1
- data/template/.github/dependabot.yml +4 -8
- data/template/.github/image-registries.sh +4 -3
- data/template/.github/workflows/dependabot-automerge.yml +29 -34
- data/template/.github/workflows/dependency-review.yml +19 -15
- data/template/.github/workflows/image.yml +65 -14
- data/template/.github/workflows/links.yml +35 -0
- data/template/.github/workflows/nix.yml +3 -3
- data/template/.github/workflows/release.yml +135 -22
- data/template/.github/workflows/ruby-ci.yml +31 -9
- data/template/.github/workflows/security.yml +15 -9
- data/template/.github/workflows/style-check-cocogitto.yml +3 -4
- data/template/.github/workflows/style-check-dprint.yml +10 -10
- data/template/.gitignore +5 -0
- data/template/.mutineer.yml +28 -0
- data/template/.rubocop.yml +49 -10
- data/template/.simplecov.erb +10 -0
- data/template/.yard-lint.yml +386 -0
- data/template/.yardopts +2 -0
- data/template/AGENTS.md.erb +76 -38
- data/template/CITATION.cff.erb +50 -0
- data/template/CODE_OF_CONDUCT.md.erb +1 -1
- data/template/CONTRIBUTING.md.erb +107 -48
- data/template/Gemfile +10 -7
- data/template/README.md.erb +12 -12
- data/template/Rakefile +167 -31
- data/template/Steepfile.erb +1 -1
- data/template/bin/setup +2 -0
- data/template/cliff.toml +40 -23
- data/template/dprint.json +1 -6
- data/template/flake.lock +3 -3
- data/template/flake.nix +25 -10
- data/template/lib/%path%/cli.rb.erb +19 -15
- data/template/lib/%path%/version.rb.erb +1 -1
- data/template/lib/%path%.rb.erb +3 -1
- data/template/sig/%path%.rbs.erb +1 -1
- data/template/test/test_%underscored%.rb.erb +4 -3
- data/template/test/test_cli.rb.erb +37 -12
- data/template/test/test_helper.rb.erb +15 -7
- metadata +16 -4
- 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.
|
|
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
|
|
40
|
-
|
|
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,
|
|
46
|
-
|
|
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
|
|
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
|
|
68
|
-
bundle exec rake checks
|
|
69
|
-
|
|
70
|
-
bundle exec rake
|
|
71
|
-
|
|
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
|
|
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
|
|
123
|
-
for the gem
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 "
|
|
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
|
-
|
|
21
|
+
gem "rubydex" # for rubocop UseProjectIndex
|
|
22
|
+
|
|
23
|
+
gem "yard"
|
|
24
|
+
gem "yard-lint"
|
|
25
|
+
|
|
23
26
|
gem "steep"
|
|
24
27
|
gem "typeprof"
|
data/template/README.md.erb
CHANGED
|
@@ -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,
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
<
|
|
65
|
-
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
sh "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
100
|
-
|
|
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
|
|
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
|