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
data/sig/veragem.rbs CHANGED
@@ -4,25 +4,73 @@ module Veragem
4
4
  class Error < Bundler::PluginError
5
5
  end
6
6
 
7
- def self.run: (Array[String] argv, ?dir: String) -> String
7
+ def self.run: (Array[String] argv, ?dir: String, ?input: Questions::_Input) -> String
8
8
 
9
9
  module Name
10
10
  PATTERN: Regexp
11
+ RESERVED: Regexp
12
+ LIBRARIES: Array[String]
11
13
  PROBE: String
12
14
 
15
+ self.@probed: Hash[[String, String, Array[String]], Array[String]]
16
+
13
17
  def self.modules: (String name) -> Array[String]
18
+ def self.path: (String name) -> String
19
+ def self.underscored: (String name) -> String
14
20
  def self.check: (String name) -> String
21
+ private def self.check_paths: (String name) -> void
15
22
  def self.check_modules: (String name) -> void
16
- def self.check_shadowing: (String name, String shadowed, String found) -> void
17
- def self.check_defined: (String name, Array[[String, String?]] defined, String? innermost) -> void
18
- def self.library_of: (String name) -> String
19
- def self.nestings_of: (String name) -> Array[String]
20
23
  def self.probe: (String library, String path, Array[String] nestings) -> Array[String]
24
+ private def self.answers: (String library, String path, Array[String] nestings) -> Array[String]
25
+ private def self.unchecked: (Array[String] nestings, String why) -> bot
26
+ private def self.check_shadowing: (String name, String shadowed, String found) -> void
27
+ private def self.check_defined: (String name, Array[[String, String?]] defined, String? innermost) -> void
28
+ private def self.library_of: (String name) -> String
29
+ private def self.nestings_of: (String name) -> Array[String]
30
+ end
31
+
32
+ module Citation
33
+ KEYS: Array[Symbol]
34
+ ORCID: Regexp
35
+ ORCID_BLOCKS: Array[Range[Integer]]
36
+
37
+ def self.cited?: (Hash[Symbol, String | bool | nil] values) -> bool
38
+ def self.check_values: (Hash[Symbol, String | bool | nil] values, bool citation) -> void
39
+ def self.check_name: (String name, bool citation) -> void
40
+ def self.check: (Symbol key, String value) { (String field, String text) -> String } -> String
41
+ def self.default: (Symbol key, Hash[Symbol, String | bool | nil] values) -> String?
42
+ private def self.names: (String field, String names) -> String
43
+ private def self.keywords: (String value) { (String keyword) -> String } -> Array[String]
44
+ private def self.orcid: (String value) -> String
45
+ private def self.valid?: (String digits) -> bool
46
+ end
47
+
48
+ module License
49
+ SPDX: Hash[String, Array[String]]
50
+ CHOICES: String
51
+
52
+ def self.check: (String value) -> String
53
+ end
54
+
55
+ module Summary
56
+ TAG: Regexp
57
+ ANNOTATION: Regexp
58
+ MAGIC: Regexp
59
+ MARKDOWN: Regexp
60
+ DASH: Regexp
61
+
62
+ def self.check: (String summary) -> String
63
+ private def self.refusal: (String summary) -> String?
64
+ private def self.left_open?: (String summary) -> bool
65
+ private def self.prose: (String summary) -> String
66
+ private def self.annotation?: (String summary) -> bool
21
67
  end
22
68
 
23
69
  class Project
24
70
  GITHUB: Regexp
25
- LICENSES: Hash[String, Array[String]]
71
+ FIELDS: Hash[Symbol, String]
72
+ MARKUP: Regexp
73
+ OPTIONAL: Hash[Symbol, String]
26
74
 
27
75
  attr_reader name: String
28
76
  attr_reader author: String
@@ -31,49 +79,129 @@ module Veragem
31
79
  attr_reader summary: String
32
80
  attr_reader license: String
33
81
  attr_reader exe: bool
82
+ attr_reader citation: bool
83
+ attr_reader given_names: String?
84
+ attr_reader family_names: String?
85
+ attr_reader alias_name: String?
86
+ attr_reader orcid: String?
87
+ attr_reader keywords: Array[String]?
34
88
 
35
- def initialize: (String name, Hash[Symbol, untyped] values) -> void
89
+ # each key's own type, which no Hash type has: Options' and Questions' hashes, asserted (#:)
90
+ # one. Steep checks each field against the Hash's values.
91
+ type values = { author: String, github: String, summary: String, license: String, ?exe: bool,
92
+ ?citation: bool, ?email: (String | false)?, ?given_names: (String | false)?,
93
+ ?family_names: String, ?alias: (String | false)?, ?orcid: String?, ?keywords: String? }
94
+
95
+ def initialize: (String name, values values) -> void
36
96
  def path: () -> String
37
97
  def underscored: () -> String
38
98
  def constant: () -> String
39
99
  def modules: () -> Array[String]
40
100
  def licenses: () -> Array[String]
41
101
  def year: () -> Integer
102
+ def today: () -> String
103
+ def yaml: (String value) -> String
42
104
  def quote: (String value) -> String
105
+ def markdown: (String value) -> String
43
106
  def nest: (String body, ?doc: String?) -> String
107
+ def self.field: (Symbol key) -> String
108
+ def self.strip: (String text) -> String
109
+ def self.quote: (String value) -> String
110
+ def self.check: (Symbol key, String? value) -> String
111
+ private def self.utf8: (Symbol key, String? value) -> String
112
+ private def self.github: (String value) -> String
113
+ private def self.text: (String field, String value) -> String
114
+ private def self.line: (String field, String value) -> String
44
115
 
45
116
  private
46
117
 
118
+ def extras: (values values) -> void
119
+ def cite: (values values) -> void
120
+ def optional: (Symbol key, (String | false)? value) -> String?
47
121
  def indent: (String code) -> String
48
- def valid: [T < Object] (String field, T value) { (T) -> bool } -> T
49
- def text: (String field, String value) -> String
122
+ end
123
+
124
+ class Questions
125
+ # what is read from: a line at a time, and whether a person types it
126
+ interface _Input
127
+ def gets: () -> String?
128
+ def tty?: () -> bool
129
+ end
130
+
131
+ interface _Output
132
+ def print: (String) -> void
133
+ def puts: (?String) -> void
134
+ end
135
+
136
+ QUESTIONS: Hash[Symbol, String]
137
+
138
+ @input: _Input
139
+ @output: _Output
140
+ @dir: String
141
+ @git: Hash[Symbol, String]
142
+
143
+ def self.none?: (String text) -> bool
144
+ def initialize: (_Input input, _Output output, String dir, ?git: Hash[Symbol, String]) -> void
145
+ def name: (String? given, exe: bool, citation: bool) -> String
146
+ def values: (String name, Hash[Symbol, String | bool] given, Hash[Symbol, String] defaults)
147
+ -> Project::values
148
+
149
+ private
150
+
151
+ def check_name: (String name, bool exe, bool citation) -> String
152
+ def answer: (Symbol key, String text, String name, Hash[Symbol, String | bool | nil] values) -> (String | bool)
153
+ def default: (Symbol key, Hash[Symbol, String] defaults, Hash[Symbol, String | bool | nil] values) -> String?
154
+ def source: (Symbol key) -> String
155
+ def ask: (String question, ?String? default, check: ^(String) -> (String | bool), ?from: String, ?optional: bool)
156
+ -> (String | bool | nil)
157
+ def offered: (String question, String? default, String from, ^(String) -> (String | bool) check) -> String?
158
+ def read: (String question, String? default, bool optional) -> String?
159
+ def unanswered: (String question) -> nil
160
+ def checked: (String question, String answer, ^(String) -> (String | bool) check, ?String note) -> (String | bool | nil)
161
+ def terminal: [T] (String question) { () -> T } -> T
162
+ def yes?: (String answer) -> bool
50
163
  end
51
164
 
52
165
  module Options
53
166
  GIT_CONFIG: Hash[Symbol, String]
167
+ DEFAULTS: Hash[Symbol, String]
168
+ BANNER: String
169
+ OPTIONS: Array[[String, String]]
54
170
 
55
- def self.parse: (Array[String] argv, ?config: ^(String) -> String?) -> Project
56
- def self.given: (Hash[Symbol, untyped] values) -> Hash[Symbol, untyped]
57
- def self.parser: () -> OptionParser
58
- def self.git_config: (String key) -> String?
171
+ def self.parse: (Array[String] argv, ?dir: String, ?config: ^(String) -> String?, ?input: Questions::_Input,
172
+ ?output: Questions::_Output) -> Project
173
+ private def self.arguments: (Array[String] argv) -> [Array[String], Hash[Symbol, String | bool]]
174
+ private def self.checked: (Symbol key, String value) -> String
175
+ private def self.ask: (Array[String] names, Hash[Symbol, String | bool] given, Hash[Symbol, String] defaults,
176
+ Questions questions) -> Project
177
+ private def self.name_of: (Array[String] names) -> String
178
+ private def self.required: (Hash[Symbol, String] defaults, Hash[Symbol, String | bool] given) -> Project::values
179
+ private def self.check_present: (Hash[Symbol, String | bool | nil] values) -> void
180
+ private def self.parser: () -> OptionParser
181
+ private def self.git_config: (String key) -> String?
59
182
  end
60
183
 
61
184
  module Generator
62
185
  TEMPLATE: String
63
186
  EXE: Array[String]
187
+ EXECUTABLES: Array[String]
64
188
  GIT_LOCAL: Hash[String, nil]
65
189
  LICENSE_FILES: Hash[String, String]
190
+ CITATION: String
191
+ TOOLS_LOCK: String
66
192
 
67
- def self.files: (Project project) -> Hash[String, String]
68
- def self.skipped: (Project project) -> Array[String]
69
- def self.tools: () -> Array[String]
70
- def self.check: (Project project, Array[String] paths) -> void
71
- def self.check_command: (Project project) -> void
72
- def self.path_of: (String file, Project project) -> String
73
193
  def self.generate: (Project project, String target) -> void
74
- def self.removed_unless_done: (String target) { () -> void } -> void
75
- def self.write: (String source, String destination, Project project) -> void
76
- def self.render: (String source, Project project) -> String
77
- def self.git: (String dir, *String args) -> void
194
+ def self.check: (String name, exe: bool, citation: bool, target: String) -> String
195
+ def self.check_name: (String name, bool exe) -> void
196
+ private def self.check_target: (String target) -> void
197
+ private def self.files: (Project project) -> Hash[String, String]
198
+ private def self.templates: (bool exe) -> Array[String]
199
+ private def self.tools: (?String lock) -> Array[String]
200
+ private def self.path_of: (String file, String name) -> String
201
+ private def self.removed_unless_done: (String target) { () -> void } -> void
202
+ private def self.write: (String file, String destination, Project project) -> void
203
+ private def self.stage: (String target, Array[String] executables) -> void
204
+ private def self.render: (String source, Project project) -> String
205
+ private def self.git: (String dir, *String args) -> void
78
206
  end
79
207
  end
@@ -0,0 +1,3 @@
1
+ # Every text file LF, as checked out on Windows too (core.autocrlf): a CRLF shebang does not run,
2
+ # and RuboCop wants LF (.rubocop.yml).
3
+ * text=auto eol=lf
@@ -1,6 +1,5 @@
1
1
  #!/bin/sh
2
- # What fails fastest, so a commit stays quick: RuboCop, the shell, the workflows, dprint; the
3
- # types, the tests and the rest are `bundle exec rake checks` and CI. Checks the working tree, not
4
- # just what is staged.
2
+ # What fails fastest, so a commit stays quick; the types, the tests and the rest are
3
+ # `bundle exec rake checks` and CI. Checks the working tree, not just what is staged.
5
4
  set -eu
6
5
  bundle exec rake rubocop shell workflows dprint
@@ -10,5 +10,6 @@
10
10
 
11
11
  - [ ] Code passes local CI (`bundle exec rake checks`).
12
12
  - [ ] Core logic is extracted into pure, unit-testable functions.
13
- - [ ] Any new `# rubocop:disable` or `# steep:ignore` names its cop or diagnostic and says why it is wrong here (RuboCop's after `--`, Steep's on the line above: AGENTS.md).
13
+ - [ ] Any new `# rubocop:disable` or `# steep:ignore` names its cop or diagnostic and says why it
14
+ is wrong here (RuboCop's after `--`, Steep's on the line above: AGENTS.md).
14
15
  - [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
@@ -1,6 +1,8 @@
1
1
  # The gems (Gemfile.lock), weekly; GitHub Actions, pinned to commits, each release in a comment,
2
2
  # and Dependabot moves both (docs.github.com, "Dependabot supported ecosystems"), weekly; the
3
3
  # flake's inputs, monthly.
4
+ # No time: Dependabot picks one for the repository (docs.github.com, "Dependabot options
5
+ # reference").
4
6
  version: 2
5
7
  updates:
6
8
  - package-ecosystem: "bundler"
@@ -8,11 +10,9 @@ updates:
8
10
  schedule:
9
11
  interval: "weekly"
10
12
  day: "sunday"
11
- time: "07:00"
12
- timezone: "UTC"
13
13
  open-pull-requests-limit: 5
14
- # `chore(deps): ...` -- no changelog entry: the lock is not in the gem, whose users resolve its
15
- # gemspec's requirements themselves
14
+ # `chore(deps): ...` -- a Miscellaneous Task in the changelog, no feature or fix: the lock is not
15
+ # in the gem, whose users resolve its gemspec's requirements themselves
16
16
  commit-message:
17
17
  prefix: "chore"
18
18
  include: "scope"
@@ -38,8 +38,6 @@ updates:
38
38
  schedule:
39
39
  interval: "weekly"
40
40
  day: "sunday"
41
- time: "07:00"
42
- timezone: "UTC"
43
41
  open-pull-requests-limit: 5
44
42
  # `ci(deps): ...` -- nothing a user of the gem meets, so no changelog entry
45
43
  commit-message:
@@ -66,8 +64,6 @@ updates:
66
64
  directory: "/"
67
65
  schedule:
68
66
  interval: "monthly"
69
- time: "06:00"
70
- timezone: "UTC"
71
67
  open-pull-requests-limit: 1
72
68
  # `chore(deps): ...` -- the flake is the development shell, the checks and the Nix package,
73
69
  # nothing RubyGems ships: a command's image takes the new inputs at its next release
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # The repositories a release's image goes to, one a line, each logged in before anything is
3
3
  # pushed: GHCR's always (the job's token), Docker Hub's when both vars.DOCKERHUB_USERNAME and
4
- # secrets.DOCKERHUB_TOKEN are set -- the name vars.DOCKERHUB_REPOSITORY, else <username>/<the
5
- # GitHub repository's name>. image.yml's; it passes GH_TOKEN and DOCKERHUB_USERNAME,
4
+ # secrets.DOCKERHUB_TOKEN are set -- vars.DOCKERHUB_REPOSITORY (<namespace>/<name>), else
5
+ # <username>/<the GitHub repository's name>. image.yml's; it passes GH_TOKEN and DOCKERHUB_USERNAME,
6
6
  # DOCKERHUB_TOKEN and DOCKERHUB_REPOSITORY (empty when unset).
7
7
  set -euo pipefail
8
8
 
@@ -24,7 +24,8 @@ repository=${repository,,}
24
24
  name=${repository#*/}
25
25
  if [[ ! $repository =~ ^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*/[a-z0-9]+((_|__|-+)[a-z0-9]+)*$ ]] ||
26
26
  ((${#name} < 2 || ${#name} > 255)); then
27
- echo "error: '$repository' is no Docker Hub repository: set vars.DOCKERHUB_REPOSITORY" >&2
27
+ echo "error: '$repository' is no Docker Hub repository:" \
28
+ "set vars.DOCKERHUB_REPOSITORY to <namespace>/<name>" >&2
28
29
  exit 1
29
30
  fi
30
31
  docker login docker.io --username "$username" --password-stdin <<< "$token" >&2
@@ -1,26 +1,23 @@
1
1
  name: Dependabot Auto-Merge
2
2
 
3
- # Required repository settings (do these once):
4
- # 1. Settings → General → Allow auto-merge ← enable
5
- # 2. Settings → Branches → Branch protection rule on main:
6
- # - Require a pull request before merging, with no required approvals
7
- # (one would hold every bump for a person)
8
- # - Require status checks to pass before merging (select the real CI
9
- # jobs: every job the workflows run on every pull request -- not one
10
- # whose `pull_request:` has `paths:`, which reports only when those
11
- # paths change, so would block every other merge)
3
+ # Repository settings, once:
4
+ # 1. Settings → General → Allow auto-merge: on.
5
+ # 2. Settings → Branches → a branch protection rule on main, its bypass to admins left allowed
6
+ # (`rake release` pushes its commit and tag to main):
7
+ # - Require a pull request before merging, no required approvals (one would hold every bump
8
+ # for a person).
9
+ # - Require status checks to pass before merging: every job the workflows run on every pull
10
+ # request -- not one whose `pull_request:` has `paths:`, which reports only when those paths
11
+ # change, so would block every other merge.
12
+ # Without step 2, this workflow merges bumps no CI job gated: auto-merge waits for *required*
13
+ # checks alone.
12
14
  #
13
- # The `permissions:` below are all this workflow's token needs; the
14
- # repository's default token permissions can stay read-only, and nothing
15
- # here creates or approves a pull request. Leave the rule's bypass to
16
- # admins allowed: `rake release` pushes its commit and tag to main.
17
- #
18
- # A merge this token makes starts no workflow run (GitHub's rule for
19
- # GITHUB_TOKEN's events): main runs nothing for it, its pull request's
20
- # required checks are what gated it.
21
- #
22
- # Without step 2, this workflow merges bumps that no CI job gated:
23
- # auto-merge waits for *required* checks alone.
15
+ # The token needs only the `permissions:` below: the repository's default token permissions can
16
+ # stay read-only, and nothing here creates or approves a pull request. A merge this token makes
17
+ # starts no workflow run (GitHub's rule for GITHUB_TOKEN's events): main runs nothing for it; its
18
+ # pull request's required checks gated it, on the main its branch was last updated from. "Require
19
+ # branches to be up to date before merging" makes that main the current one, each bump then waiting
20
+ # for its branch's update (`@dependabot rebase`).
24
21
 
25
22
  on:
26
23
  pull_request:
@@ -29,14 +26,14 @@ permissions: {}
29
26
 
30
27
  jobs:
31
28
  dependabot:
29
+ name: Auto-merge
32
30
  runs-on: ubuntu-latest
33
31
  permissions:
34
- contents: write
35
- pull-requests: write
36
- # user.login, not github.actor: the account that opened the pull request,
37
- # not whoever caused the event (a person pushing to Dependabot's branch
38
- # too).
39
- # Ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions
32
+ contents: write # the merge
33
+ pull-requests: write # auto-merge, set on the pull request
34
+ # user.login, not github.actor: the account that opened the pull request, not whoever caused
35
+ # the event (a person pushing to Dependabot's branch too).
36
+ # Ref: https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/automate-dependabot-with-actions
40
37
  # github.actor: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts
41
38
  #
42
39
  # To keep forks from auto-merging their own bumps, pin the repository:
@@ -51,14 +48,12 @@ jobs:
51
48
  github-token: ${{ secrets.GITHUB_TOKEN }}
52
49
 
53
50
  - name: Enable auto-merge for patch and minor updates
54
- # update-type is the highest version-update:semver-{major,minor,patch}
55
- # among the updated dependencies whose level is known (Dependabot's
56
- # metadata, else their versions); empty when none is, which this
57
- # condition refuses. A security update carries the same labels, no
58
- # "security:" prefix. To gate on security status, enable alert-lookup
59
- # with a PAT or an App's token (a Dependabot secret: a run Dependabot
60
- # starts reads no other) and check steps.metadata.outputs.alert-state
61
- # (the first dependency's alert).
51
+ # update-type: the highest version-update:semver-{major,minor,patch} among the updated
52
+ # dependencies whose level is known (Dependabot's metadata, else their versions); empty
53
+ # when none is, which this condition refuses. A security update carries the same labels,
54
+ # no "security:" prefix. To gate on security status, enable alert-lookup with a PAT or an
55
+ # App's token (a Dependabot secret: a run Dependabot starts reads no other) and check
56
+ # steps.metadata.outputs.alert-state (the first dependency's alert).
62
57
  if: >-
63
58
  contains(
64
59
  fromJSON('["version-update:semver-patch", "version-update:semver-minor"]'),
@@ -1,9 +1,9 @@
1
1
  name: Dependency Review
2
2
 
3
- # The licences and advisories of what a pull request adds to Gemfile.lock and the workflows, as the
4
- # templates' Python and Rust ones review theirs. Advisory: it reports only when those files change,
5
- # so no required check can be it, and dependabot-automerge.yml merges a bump it flags (security.yml's
6
- # bundler-audit holds the lock daily).
3
+ # The licences and advisories of what a pull request adds to Gemfile.lock and the workflows.
4
+ # Advisory: it reports only when those files change, so no required check can be it, and
5
+ # dependabot-automerge.yml merges a bump it flags (security.yml's bundler-audit holds the lock
6
+ # daily).
7
7
  on:
8
8
  pull_request:
9
9
  paths:
@@ -12,18 +12,22 @@ on:
12
12
  - "*.gemspec"
13
13
  - ".github/workflows/**"
14
14
 
15
+ # a pull request's newest run cancels its older ones; any other run, a group of its own: every
16
+ # commit on main checked (in a shared group, a pending run gives way to the newest: the default)
17
+ concurrency:
18
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
19
+ cancel-in-progress: true
20
+
15
21
  permissions: {}
16
22
 
17
- # NOTE: it needs the repository's dependency graph (Settings → Advanced
18
- # Security), and on a private repository GitHub Code Security too (an
19
- # organization's): without them, this job fails rather than skipping --
20
- # enable them, or delete the workflow.
23
+ # NOTE: it needs the repository's dependency graph (Settings → Advanced Security), and on a
24
+ # private repository an organization's GitHub Code Security too: without them the job fails, not
25
+ # skips -- enable them, or delete the workflow.
21
26
  #
22
- # What it reads: Gemfile.lock has no scopes, so each of its gems counts as
23
- # runtime -- the gemspec's dependencies and the Gemfile's tools alike --;
24
- # the actions for licences in effect: advisories match actions by semantic
25
- # version alone, not the commits these are pinned to ("Dependency graph
26
- # supported package ecosystems").
27
+ # Gemfile.lock has no scopes, so each of its gems counts as runtime, the gemspec's dependencies and
28
+ # the Gemfile's tools alike. In effect, the actions are reviewed for licences alone: advisories
29
+ # match an action by semantic version, not the commit it is pinned to ("Dependency graph supported
30
+ # package ecosystems").
27
31
  jobs:
28
32
  dependency-review:
29
33
  name: License & Security Check
@@ -38,8 +42,8 @@ jobs:
38
42
  fail-on-severity: moderate
39
43
  # The licences are ClearlyDefined's, not the gemspecs'; a gem's:
40
44
  # curl -s https://api.clearlydefined.io/definitions/gem/rubygems/-/NAME/VERSION
41
- # (.licensed.declared). Add anything legitimate that is missing rather
42
- # than deleting this list -- an empty allow-list allows everything.
45
+ # (.licensed.declared). Add what is legitimate and missing; never delete this list: an
46
+ # empty allow-list allows everything.
43
47
  # Ruby: Ruby's own licence, with MIT or BSD's (concurrent-ruby, reline)
44
48
  allow-licenses: >
45
49
  0BSD, Apache-2.0, BlueOak-1.0.0, BSD-2-Clause, BSD-3-Clause,
@@ -5,13 +5,19 @@
5
5
  # <version>-arm64) stay, the multi-architecture tag's parts. It runs beside release.yml, not after
6
6
  # it: the image does not wait for the gem's push to RubyGems.
7
7
  #
8
- # The job's token pushes: no secret stored. A first push is private (GHCR's default): make the
9
- # package public in its settings once.
8
+ # The jobs' token pushes to GHCR: no secret stored. A first push is private (GHCR's default): make
9
+ # the package public in its settings once.
10
10
  #
11
11
  # Docker Hub too, the same tags, once vars.DOCKERHUB_USERNAME and secrets.DOCKERHUB_TOKEN (an
12
12
  # access token that writes) are set: as vars.DOCKERHUB_REPOSITORY, else <username>/<repository>. A
13
- # Docker Hub failure in the build (its login, its name, a push) holds GHCR's multi-architecture tag
14
- # back too; a re-run, once fixed, completes both.
13
+ # Docker Hub failure in a push (its login, its name) holds GHCR's multi-architecture tag back too;
14
+ # once fixed, a re-run of the failed jobs within a day (the image's artifact kept so long), or of
15
+ # all of them, completes both.
16
+ #
17
+ # The build, which runs the image, holds no write and no secret, and passes on the image it ran;
18
+ # the jobs that push, which build nothing, run in the `image` environment, which holds
19
+ # DOCKERHUB_TOKEN and which the generated CONTRIBUTING.md's first steps limit to the release tags: a
20
+ # job of no other ref can read it.
15
21
  #
16
22
  # Run by hand (workflow_dispatch) on a branch, it builds and runs the image on both architectures,
17
23
  # pushing nothing: a rehearsal before the first release; on a tag, it publishes that release again.
@@ -37,8 +43,8 @@ jobs:
37
43
  arch: arm64
38
44
  permissions:
39
45
  contents: read
40
- packages: write
41
46
  outputs:
47
+ image: ${{ steps.build.outputs.image }}
42
48
  description: ${{ steps.build.outputs.description }}
43
49
  steps:
44
50
  - name: Checkout
@@ -47,7 +53,7 @@ jobs:
47
53
  persist-credentials: false
48
54
 
49
55
  - name: Install Nix
50
- uses: cachix/install-nix-action@448cde739abec1eaf0646249e3d25bb8d9bc7702 # v31.11.2
56
+ uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31.11.3
51
57
 
52
58
  - name: Build and run
53
59
  id: build
@@ -59,9 +65,10 @@ jobs:
59
65
  echo "::error::$GITHUB_REF_NAME, but version.rb declares $version" >&2
60
66
  exit 1
61
67
  fi
62
- # streamed into docker: no compressed copy of the closure in the store
68
+ # streamed: no compressed copy of the closure in the store
63
69
  stream="$(nix build --no-link --print-out-paths .#image.stream)"
64
- "$stream" | docker load
70
+ "$stream" > image.tar
71
+ docker load --input image.tar
65
72
  loaded="$(nix eval --raw .#image.imageName):$version"
66
73
  # the image as a user runs it: its entrypoint, as its user
67
74
  expected="$(nix eval --raw .#image.imageName) $version"
@@ -73,10 +80,52 @@ jobs:
73
80
  # assigned first: a failing command substitution inside echo's arguments would not stop
74
81
  # the step
75
82
  description="$(nix eval --raw .#default.meta.description)"
76
- echo "description=$description" >> "$GITHUB_OUTPUT"
83
+ {
84
+ echo "image=$loaded"
85
+ echo "description=$description"
86
+ } >> "$GITHUB_OUTPUT"
77
87
 
78
- - name: Push
88
+ - name: Keep the image
79
89
  if: github.ref_type == 'tag'
90
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
91
+ with:
92
+ name: image-${{ matrix.arch }}
93
+ path: image.tar
94
+ if-no-files-found: error
95
+ retention-days: 1
96
+
97
+ push:
98
+ name: Push (${{ matrix.arch }})
99
+ needs: build
100
+ if: github.ref_type == 'tag'
101
+ runs-on: ${{ matrix.runner }}
102
+ environment:
103
+ name: image
104
+ strategy:
105
+ matrix:
106
+ include:
107
+ - runner: ubuntu-latest
108
+ arch: amd64
109
+ - runner: ubuntu-24.04-arm
110
+ arch: arm64
111
+ permissions:
112
+ contents: read
113
+ packages: write # GHCR's pushes
114
+ steps:
115
+ - name: Checkout
116
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
117
+ with:
118
+ persist-credentials: false
119
+
120
+ - name: The build's image
121
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
122
+ with:
123
+ name: image-${{ matrix.arch }}
124
+
125
+ - name: Load it
126
+ run: docker load --input image.tar
127
+
128
+ - name: Push
80
129
  env:
81
130
  GH_TOKEN: ${{ github.token }}
82
131
  # Docker Hub too, when both are set (.github/image-registries.sh): an unset variable or
@@ -86,25 +135,27 @@ jobs:
86
135
  DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
87
136
  DOCKERHUB_REPOSITORY: ${{ vars.DOCKERHUB_REPOSITORY }}
88
137
  ARCH: ${{ matrix.arch }}
138
+ LOADED: ${{ needs.build.outputs.image }}
89
139
  run: |
90
140
  set -euo pipefail
91
141
  repositories="$(.github/image-registries.sh)"
92
142
  version="${GITHUB_REF_NAME#v}"
93
- loaded="$(nix eval --raw .#image.imageName):$version"
94
143
  # the list on its own descriptor: no command in the loop can read it away
95
144
  while read -r repository <&3; do
96
- docker tag "$loaded" "$repository:$version-$ARCH"
145
+ docker tag "$LOADED" "$repository:$version-$ARCH"
97
146
  docker push "$repository:$version-$ARCH"
98
147
  done 3<<< "$repositories"
99
148
 
100
149
  publish:
101
150
  name: Multi-architecture tag
102
- needs: build
151
+ needs: [build, push]
103
152
  if: github.ref_type == 'tag'
104
153
  runs-on: ubuntu-latest
154
+ environment:
155
+ name: image
105
156
  permissions:
106
157
  contents: read
107
- packages: write
158
+ packages: write # GHCR's multi-architecture tag
108
159
  steps:
109
160
  - name: Checkout
110
161
  # every tag, to tell whether this one is the highest
@@ -0,0 +1,35 @@
1
+ # The links of the files git tracks but the tests' (`rake links`, lychee), monthly and on demand: a
2
+ # page moves or goes with nothing changed here. In the dev shell, whose lychee is the flake's;
3
+ # GITHUB_TOKEN, so GitHub's links are not rate-limited, lychee's alone, not the gems' installs.
4
+ name: Links
5
+
6
+ on:
7
+ schedule:
8
+ - cron: "47 4 1 * *" # the 1st of each month, 04:47 UTC: off the hour, when GitHub delays runs
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ links:
16
+ name: lychee
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
+ with:
22
+ persist-credentials: false
23
+
24
+ - name: Install Nix
25
+ uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31.11.3
26
+
27
+ - name: Install the gems
28
+ env:
29
+ BUNDLE_FROZEN: "true"
30
+ run: nix develop --command bundle install
31
+
32
+ - name: rake links
33
+ env:
34
+ GITHUB_TOKEN: ${{ github.token }}
35
+ run: nix develop --command bundle exec rake links
@@ -7,8 +7,8 @@ name: Nix
7
7
  on:
8
8
  push:
9
9
  branches: [main]
10
- # every pull request, whatever it touches: a path-filtered workflow never reports, and a
11
- # required check that never reports blocks the merge
10
+ # every pull request, whatever it touches: a path-filtered workflow does not report on one
11
+ # outside its paths, and a required check that does not report blocks the merge
12
12
  pull_request:
13
13
  workflow_dispatch:
14
14
 
@@ -37,7 +37,7 @@ jobs:
37
37
 
38
38
  - name: Install Nix
39
39
  # flakes on, and the job's token for GitHub fetches
40
- uses: cachix/install-nix-action@448cde739abec1eaf0646249e3d25bb8d9bc7702 # v31.11.2
40
+ uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31.11.3
41
41
 
42
42
  - name: nix flake check
43
43
  run: nix flake check --print-build-logs