veragem 0.1.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 +7 -0
- data/CHANGELOG.md +53 -0
- data/LICENSE-APACHE +201 -0
- data/LICENSE-MIT +7 -0
- data/README.md +151 -0
- data/lib/veragem/generator.rb +109 -0
- data/lib/veragem/name.rb +111 -0
- data/lib/veragem/options.rb +53 -0
- data/lib/veragem/project.rb +77 -0
- data/lib/veragem/version.rb +5 -0
- data/lib/veragem.rb +23 -0
- data/plugins.rb +15 -0
- data/sig/bundler.rbs +10 -0
- data/sig/veragem.rbs +79 -0
- data/template/%name%.gemspec.erb +30 -0
- data/template/.editorconfig +15 -0
- data/template/.githooks/commit-msg +12 -0
- data/template/.githooks/pre-commit +6 -0
- data/template/.githooks/pre-push +17 -0
- data/template/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- data/template/.github/ISSUE_TEMPLATE/config.yml.erb +5 -0
- data/template/.github/ISSUE_TEMPLATE/documentation.md +19 -0
- data/template/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- data/template/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- data/template/.github/dependabot.yml +85 -0
- data/template/.github/image-registries.sh +31 -0
- data/template/.github/release-notes.sh +13 -0
- data/template/.github/workflows/dependabot-automerge.yml +70 -0
- data/template/.github/workflows/dependency-review.yml +50 -0
- data/template/.github/workflows/image.yml +140 -0
- data/template/.github/workflows/nix.yml +46 -0
- data/template/.github/workflows/release.yml +117 -0
- data/template/.github/workflows/rubocop-autocorrect.yml +114 -0
- data/template/.github/workflows/ruby-ci.yml +62 -0
- data/template/.github/workflows/security.yml +43 -0
- data/template/.github/workflows/style-check-cocogitto.yml +56 -0
- data/template/.github/workflows/style-check-dprint.yml +44 -0
- data/template/.gitignore +12 -0
- data/template/.rubocop.yml +27 -0
- data/template/AGENTS.md.erb +75 -0
- data/template/CODE_OF_CONDUCT.md.erb +18 -0
- data/template/CONTRIBUTING.md.erb +183 -0
- data/template/Gemfile +24 -0
- data/template/LICENSE-APACHE.erb +201 -0
- data/template/LICENSE-MIT.erb +7 -0
- data/template/README.md.erb +82 -0
- data/template/Rakefile +136 -0
- data/template/SECURITY.md.erb +21 -0
- data/template/Steepfile.erb +11 -0
- data/template/bin/console.erb +8 -0
- data/template/bin/setup +5 -0
- data/template/cliff.toml +70 -0
- data/template/cog.toml +8 -0
- data/template/dprint.json +23 -0
- data/template/exe/%name%.erb +6 -0
- data/template/flake.lock +27 -0
- data/template/flake.nix +213 -0
- data/template/lib/%path%/cli.rb.erb +45 -0
- data/template/lib/%path%/version.rb.erb +3 -0
- data/template/lib/%path%.rb.erb +13 -0
- data/template/sig/%path%.rbs.erb +16 -0
- data/template/test/test_%underscored%.rb.erb +27 -0
- data/template/test/test_cli.rb.erb +40 -0
- data/template/test/test_helper.rb.erb +15 -0
- data/template/typeprof.conf.jsonc +7 -0
- metadata +111 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Veragem
|
|
6
|
+
# The command line, read into a Project; git's config gives the author, the email and the GitHub
|
|
7
|
+
# user, as `bundle gem` reads them.
|
|
8
|
+
module Options
|
|
9
|
+
GIT_CONFIG = { author: "user.name", email: "user.email", github: "github.user" }.freeze
|
|
10
|
+
|
|
11
|
+
def self.parse(argv, config: ->(key) { git_config(key) })
|
|
12
|
+
values = GIT_CONFIG.transform_values { config.call(it) }
|
|
13
|
+
# an argument of invalid UTF-8 as bytes, as git's values are: OptionParser's matching raises on
|
|
14
|
+
# it, Project refuses it
|
|
15
|
+
names = parser.permute(argv.map { it.valid_encoding? ? it : it.b }, into: values)
|
|
16
|
+
raise Error, "one gem name, not #{names.size} (bundle veragem --help)" unless names.size == 1
|
|
17
|
+
|
|
18
|
+
# rubocop:disable-next Lint/NameTypo -- the project index lacks Class#new, and takes Project#nest for it
|
|
19
|
+
Project.new(names.fetch(0), { summary: "A Ruby gem.", license: "dual", **given(values) })
|
|
20
|
+
rescue OptionParser::ParseError => e
|
|
21
|
+
raise Error, "#{e.message} (bundle veragem --help)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The values set, the author and the GitHub user required.
|
|
25
|
+
def self.given(values)
|
|
26
|
+
%i[author github].each { raise Error, "no #{it}: --#{it}, or git config #{GIT_CONFIG[it]}" unless values[it] }
|
|
27
|
+
values.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.parser
|
|
31
|
+
OptionParser.new("Usage: bundle veragem NAME [options]") do |opts|
|
|
32
|
+
opts.program_name = "bundle veragem"
|
|
33
|
+
opts.version = VERSION
|
|
34
|
+
opts.on("--exe", "Add a command line, exe/NAME")
|
|
35
|
+
opts.on("--license=LICENSE", Project::LICENSES.keys, "mit, apache or dual (MIT OR Apache-2.0, the default)")
|
|
36
|
+
opts.on("--author=NAME", "The author (default: git config user.name)")
|
|
37
|
+
opts.on("--email=EMAIL", "The author's email (default: git config user.email)")
|
|
38
|
+
opts.on("--github=USER", "The GitHub user or organization (default: git config github.user)")
|
|
39
|
+
opts.on("--summary=TEXT", "The gem's summary (default: A Ruby gem.)")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A git config value, nil when unset or git is missing: its bytes, which Project reads as UTF-8
|
|
44
|
+
# whatever the locale.
|
|
45
|
+
def self.git_config(key)
|
|
46
|
+
command = ["git", "config", "--get", key]
|
|
47
|
+
value = IO.popen(command, err: File::NULL, external_encoding: Encoding::BINARY, &:read).strip
|
|
48
|
+
value unless value.empty?
|
|
49
|
+
rescue SystemCallError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veragem
|
|
4
|
+
# The gem to generate: the values given, checked, and what follows from them.
|
|
5
|
+
class Project
|
|
6
|
+
# GitHub's rule for a user or organization name.
|
|
7
|
+
GITHUB = /\A[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}\z/i
|
|
8
|
+
# Each choice's SPDX identifiers: the gemspec's licenses, the LICENSE-* files.
|
|
9
|
+
LICENSES = { "mit" => ["MIT"], "apache" => ["Apache-2.0"], "dual" => ["MIT", "Apache-2.0"] }.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :name, :author, :email, :github, :summary, :license, :exe
|
|
12
|
+
|
|
13
|
+
# +name+, and +values+: the author, email, github, summary and license (strings), exe (true or
|
|
14
|
+
# absent), as Options reads them.
|
|
15
|
+
def initialize(name, values)
|
|
16
|
+
@name = Name.check(name)
|
|
17
|
+
@author = text("author", values[:author])
|
|
18
|
+
@email = values[:email] && text("email", values[:email])
|
|
19
|
+
@github = valid("GitHub user", values[:github]) { GITHUB.match?(it) }
|
|
20
|
+
@summary = text("summary", values[:summary])
|
|
21
|
+
@license = valid("license (mit, apache or dual)", values[:license]) { LICENSES.key?(it) }
|
|
22
|
+
@exe = values[:exe] == true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The file the gem requires: `my-gem`'s is my/gem.
|
|
26
|
+
def path = name.tr("-", "/")
|
|
27
|
+
|
|
28
|
+
def underscored = name.tr("-", "_")
|
|
29
|
+
|
|
30
|
+
# Its module, as `bundle gem` names it.
|
|
31
|
+
def constant = modules.join("::")
|
|
32
|
+
|
|
33
|
+
def modules = Name.modules(name)
|
|
34
|
+
|
|
35
|
+
def licenses = LICENSES.fetch(license)
|
|
36
|
+
|
|
37
|
+
def year = Time.now.year
|
|
38
|
+
|
|
39
|
+
# +value+ as a Ruby string literal, as written whatever the locale (String#inspect escapes
|
|
40
|
+
# what the locale cannot show): a quote, a backslash and an interpolation escaped.
|
|
41
|
+
def quote(value) = %("#{value.gsub(/["\\]|#(?=[{$@])/) { "\\#{it}" }}")
|
|
42
|
+
|
|
43
|
+
# +body+ inside the gem's modules, each nested and indented, +doc+ a comment on the innermost.
|
|
44
|
+
def nest(body, doc: nil)
|
|
45
|
+
*outer, inner = modules
|
|
46
|
+
code = "#{"# #{doc}\n" if doc}module #{inner}\n#{indent(body)}end\n"
|
|
47
|
+
outer.reverse.reduce(code) { |nested, mod| "module #{mod}\n#{indent(nested)}end\n" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def indent(code) = code.gsub(/^(?=.)/, " ")
|
|
53
|
+
|
|
54
|
+
def valid(field, value)
|
|
55
|
+
raise Error, "invalid #{field} #{value.inspect}" unless yield value
|
|
56
|
+
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A value a file quotes: one line of valid UTF-8, its surrounding spaces stripped (Unicode's too),
|
|
61
|
+
# short enough for the gemspec's line (RuboCop's 120 columns), which RubyGems does not refuse as
|
|
62
|
+
# unfinished.
|
|
63
|
+
def text(field, value)
|
|
64
|
+
value = value.dup.force_encoding(Encoding::UTF_8)
|
|
65
|
+
one_line = value.valid_encoding? && !value.match?(/[[:cntrl:]\u2028\u2029]/)
|
|
66
|
+
line = one_line ? value.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "") : ""
|
|
67
|
+
raise Error, "invalid #{field} #{value.inspect}: one line of text" if line.empty?
|
|
68
|
+
raise Error, "invalid #{field}: too long, its Ruby string at most 100 characters" if quote(line).length > 100
|
|
69
|
+
if line.match?(/\A(TODO|FIXME)/)
|
|
70
|
+
raise Error,
|
|
71
|
+
"invalid #{field} #{line.inspect}: RubyGems refuses one starting TODO or FIXME"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
line
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/veragem.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require_relative "veragem/version"
|
|
5
|
+
require_relative "veragem/name"
|
|
6
|
+
require_relative "veragem/project"
|
|
7
|
+
require_relative "veragem/options"
|
|
8
|
+
require_relative "veragem/generator"
|
|
9
|
+
|
|
10
|
+
# `bundle veragem NAME`: a new gem, its toolchain wired up.
|
|
11
|
+
module Veragem
|
|
12
|
+
# A refusal: Bundler prints its message alone, and exits with its status.
|
|
13
|
+
class Error < Bundler::PluginError; end
|
|
14
|
+
|
|
15
|
+
# Generates the gem +argv+ names into +dir+; returns its directory.
|
|
16
|
+
def self.run(argv, dir: Dir.pwd)
|
|
17
|
+
project = Options.parse(argv)
|
|
18
|
+
target = File.join(dir, project.name)
|
|
19
|
+
Generator.generate(project, target)
|
|
20
|
+
puts "Created #{project.name}. Next: cd #{project.name} && bin/setup (CONTRIBUTING.md's first steps)"
|
|
21
|
+
target
|
|
22
|
+
end
|
|
23
|
+
end
|
data/plugins.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler loads this file to find the plugin's command: `bundle veragem`.
|
|
4
|
+
require "veragem" # Bundler puts the plugin's lib/ on the load path first
|
|
5
|
+
|
|
6
|
+
module Veragem
|
|
7
|
+
# The command, Bundler's plugin API.
|
|
8
|
+
class Command < Bundler::Plugin::API
|
|
9
|
+
command "veragem"
|
|
10
|
+
|
|
11
|
+
def exec(_command, args)
|
|
12
|
+
Veragem.run(args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/sig/bundler.rbs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The part of Bundler the plugin uses: Bundler ships no signatures, nor does gem_rbs_collection.
|
|
2
|
+
module Bundler
|
|
3
|
+
def self.unbundled_env: () -> Hash[String, String]
|
|
4
|
+
|
|
5
|
+
class BundlerError < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class PluginError < BundlerError
|
|
9
|
+
end
|
|
10
|
+
end
|
data/sig/veragem.rbs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Veragem
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < Bundler::PluginError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.run: (Array[String] argv, ?dir: String) -> String
|
|
8
|
+
|
|
9
|
+
module Name
|
|
10
|
+
PATTERN: Regexp
|
|
11
|
+
PROBE: String
|
|
12
|
+
|
|
13
|
+
def self.modules: (String name) -> Array[String]
|
|
14
|
+
def self.check: (String name) -> String
|
|
15
|
+
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
|
+
def self.probe: (String library, String path, Array[String] nestings) -> Array[String]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Project
|
|
24
|
+
GITHUB: Regexp
|
|
25
|
+
LICENSES: Hash[String, Array[String]]
|
|
26
|
+
|
|
27
|
+
attr_reader name: String
|
|
28
|
+
attr_reader author: String
|
|
29
|
+
attr_reader email: String?
|
|
30
|
+
attr_reader github: String
|
|
31
|
+
attr_reader summary: String
|
|
32
|
+
attr_reader license: String
|
|
33
|
+
attr_reader exe: bool
|
|
34
|
+
|
|
35
|
+
def initialize: (String name, Hash[Symbol, untyped] values) -> void
|
|
36
|
+
def path: () -> String
|
|
37
|
+
def underscored: () -> String
|
|
38
|
+
def constant: () -> String
|
|
39
|
+
def modules: () -> Array[String]
|
|
40
|
+
def licenses: () -> Array[String]
|
|
41
|
+
def year: () -> Integer
|
|
42
|
+
def quote: (String value) -> String
|
|
43
|
+
def nest: (String body, ?doc: String?) -> String
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
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
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module Options
|
|
53
|
+
GIT_CONFIG: Hash[Symbol, String]
|
|
54
|
+
|
|
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?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module Generator
|
|
62
|
+
TEMPLATE: String
|
|
63
|
+
EXE: Array[String]
|
|
64
|
+
GIT_LOCAL: Hash[String, nil]
|
|
65
|
+
LICENSE_FILES: Hash[String, String]
|
|
66
|
+
|
|
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
|
+
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
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/<%= path %>/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "<%= name %>"
|
|
7
|
+
spec.version = <%= constant %>::VERSION
|
|
8
|
+
spec.authors = [<%= quote(author) %>]
|
|
9
|
+
<%- if email -%>
|
|
10
|
+
spec.email = [<%= quote(email) %>]
|
|
11
|
+
<%- end -%>
|
|
12
|
+
|
|
13
|
+
spec.summary = <%= quote(summary) %>
|
|
14
|
+
spec.homepage = "https://github.com/<%= github %>/<%= name %>"
|
|
15
|
+
spec.licenses = <%= licenses.inspect %>
|
|
16
|
+
spec.required_ruby_version = ">= 4.0"
|
|
17
|
+
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
22
|
+
|
|
23
|
+
# What the gem ships: its code, its signatures and its documents -- not the tests or the tooling.
|
|
24
|
+
spec.files = Dir.glob(%w[lib/**/*.rb sig/**/*.rbs exe/* README.md CHANGELOG.md LICENSE-*], base: __dir__)
|
|
25
|
+
<%- if exe -%>
|
|
26
|
+
spec.bindir = "exe"
|
|
27
|
+
spec.executables = ["<%= name %>"]
|
|
28
|
+
<%- end -%>
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
# shfmt's style (it reads this when given no formatting flag): the shell scripts, and the git
|
|
4
|
+
# hooks, which have no extension
|
|
5
|
+
[*.sh]
|
|
6
|
+
indent_style = space
|
|
7
|
+
indent_size = 2
|
|
8
|
+
switch_case_indent = true
|
|
9
|
+
space_redirects = true
|
|
10
|
+
|
|
11
|
+
[.githooks/*]
|
|
12
|
+
indent_style = space
|
|
13
|
+
indent_size = 2
|
|
14
|
+
switch_case_indent = true
|
|
15
|
+
space_redirects = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The message, as CI's Conventional Commits check will read it -- a merge
|
|
3
|
+
# commit's excepted, as CI excepts it (cog.toml's ignore_merge_commits).
|
|
4
|
+
set -eu
|
|
5
|
+
if git rev-parse -q --verify MERGE_HEAD > /dev/null; then exit 0; fi
|
|
6
|
+
# `cog verify` reads the author from git's config -- GIT_AUTHOR_NAME and the like are not
|
|
7
|
+
# config -- and panics without it
|
|
8
|
+
if ! git config user.name > /dev/null || ! git config user.email > /dev/null; then
|
|
9
|
+
echo "commit-msg: cog verify needs git's user.name and user.email (git config)" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
cog verify --file "$1"
|
|
@@ -0,0 +1,6 @@
|
|
|
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.
|
|
5
|
+
set -eu
|
|
6
|
+
bundle exec rake rubocop shell workflows dprint
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The history of every pushed ref, as CI checks main's: `git revert` and
|
|
3
|
+
# `git cherry-pick` commit without running commit-msg, and their default
|
|
4
|
+
# messages (`Revert "..."`) are not Conventional Commits.
|
|
5
|
+
set -eu
|
|
6
|
+
while read -r line; do
|
|
7
|
+
# <local ref> <local oid> <remote ref> <remote oid>, read from the right:
|
|
8
|
+
# the local ref is as typed, and may hold spaces (`main@{1 day ago}`)
|
|
9
|
+
line="${line% *}"
|
|
10
|
+
line="${line% *}"
|
|
11
|
+
local_oid="${line##* }"
|
|
12
|
+
case "$local_oid" in
|
|
13
|
+
# `..<commit>`: all its history, as `cog check` reads HEAD's
|
|
14
|
+
*[!0]*) cog check "..$(git rev-parse "$local_oid^{commit}")" ;;
|
|
15
|
+
*) ;; # a deletion: nothing is pushed
|
|
16
|
+
esac
|
|
17
|
+
done
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug Report
|
|
3
|
+
about: Report incorrect or unexpected behaviour
|
|
4
|
+
title: "bug: "
|
|
5
|
+
labels: "bug"
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## System Context
|
|
10
|
+
|
|
11
|
+
- **OS / kernel version:**
|
|
12
|
+
- **Ruby version (`ruby -v`):**
|
|
13
|
+
- **Gem version or commit:**
|
|
14
|
+
|
|
15
|
+
## Expected Behavior
|
|
16
|
+
|
|
17
|
+
[Describe what should have happened.]
|
|
18
|
+
|
|
19
|
+
## Actual Behavior
|
|
20
|
+
|
|
21
|
+
[Describe the failure. Include raw terminal output, tracebacks, or logs.]
|
|
22
|
+
|
|
23
|
+
## Reproduction
|
|
24
|
+
|
|
25
|
+
1. [Step 1]
|
|
26
|
+
2. [Step 2]
|
|
27
|
+
3. [Command executed]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Documentation / Meta
|
|
3
|
+
about: Fix documentation or internal repository rules
|
|
4
|
+
title: "docs: "
|
|
5
|
+
labels: "documentation"
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Target
|
|
10
|
+
|
|
11
|
+
[Which file is being modified?]
|
|
12
|
+
|
|
13
|
+
## Issue
|
|
14
|
+
|
|
15
|
+
[What is wrong or missing?]
|
|
16
|
+
|
|
17
|
+
## Proposed Change
|
|
18
|
+
|
|
19
|
+
[Briefly describe the correction.]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Architecture Proposal / Feature
|
|
3
|
+
about: Propose a structural change or new capability
|
|
4
|
+
title: "feat: "
|
|
5
|
+
labels: "enhancement"
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Technical Proposal
|
|
10
|
+
|
|
11
|
+
[Describe the proposed implementation or architectural change.]
|
|
12
|
+
|
|
13
|
+
## Justification
|
|
14
|
+
|
|
15
|
+
[Why is this necessary? What does it make possible that is not possible today?]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Problem
|
|
2
|
+
|
|
3
|
+
[Describe the exact bug, limitation, or requirement this PR addresses.]
|
|
4
|
+
|
|
5
|
+
## Proposed Solution
|
|
6
|
+
|
|
7
|
+
[Describe the change and anything about the approach that is not obvious from the diff.]
|
|
8
|
+
|
|
9
|
+
## Verification
|
|
10
|
+
|
|
11
|
+
- [ ] Code passes local CI (`bundle exec rake checks`).
|
|
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).
|
|
14
|
+
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# The gems (Gemfile.lock), weekly; GitHub Actions, pinned to commits, each release in a comment,
|
|
2
|
+
# and Dependabot moves both (docs.github.com, "Dependabot supported ecosystems"), weekly; the
|
|
3
|
+
# flake's inputs, monthly.
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: "bundler"
|
|
7
|
+
directory: "/"
|
|
8
|
+
schedule:
|
|
9
|
+
interval: "weekly"
|
|
10
|
+
day: "sunday"
|
|
11
|
+
time: "07:00"
|
|
12
|
+
timezone: "UTC"
|
|
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
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: "chore"
|
|
18
|
+
include: "scope"
|
|
19
|
+
labels: # each created once (Issues > Labels): one the repository lacks is ignored
|
|
20
|
+
- "dependencies"
|
|
21
|
+
- "ruby"
|
|
22
|
+
groups:
|
|
23
|
+
# minor and patch updates in one pull request (dependabot-automerge.yml merges it once the
|
|
24
|
+
# checks pass); a major one alone, for review
|
|
25
|
+
gems:
|
|
26
|
+
patterns:
|
|
27
|
+
- "*"
|
|
28
|
+
update-types:
|
|
29
|
+
- "minor"
|
|
30
|
+
- "patch"
|
|
31
|
+
# a release a week old at least, time for a compromised one to be found and yanked first
|
|
32
|
+
# (zizmor's minimum; Dependabot's default is 3 days). A security update is not held back
|
|
33
|
+
cooldown:
|
|
34
|
+
default-days: 7
|
|
35
|
+
|
|
36
|
+
- package-ecosystem: "github-actions"
|
|
37
|
+
directory: "/"
|
|
38
|
+
schedule:
|
|
39
|
+
interval: "weekly"
|
|
40
|
+
day: "sunday"
|
|
41
|
+
time: "07:00"
|
|
42
|
+
timezone: "UTC"
|
|
43
|
+
open-pull-requests-limit: 5
|
|
44
|
+
# `ci(deps): ...` -- nothing a user of the gem meets, so no changelog entry
|
|
45
|
+
commit-message:
|
|
46
|
+
prefix: "ci"
|
|
47
|
+
include: "scope"
|
|
48
|
+
labels:
|
|
49
|
+
- "dependencies"
|
|
50
|
+
- "actions"
|
|
51
|
+
groups:
|
|
52
|
+
actions-dependencies:
|
|
53
|
+
patterns:
|
|
54
|
+
- "*"
|
|
55
|
+
update-types:
|
|
56
|
+
- "minor"
|
|
57
|
+
- "patch"
|
|
58
|
+
cooldown:
|
|
59
|
+
default-days: 7
|
|
60
|
+
|
|
61
|
+
# the flake's inputs, monthly, in one pull request (Dependabot's `nix` ecosystem: each input to
|
|
62
|
+
# its branch's newest commit). A person merges it: dependabot-automerge.yml takes patch and minor
|
|
63
|
+
# bumps alone, and a commit's move is neither. An input in a private repository is not moved
|
|
64
|
+
# (docs.github.com, "Supported ecosystems and repositories"): `nix flake update` by hand for it
|
|
65
|
+
- package-ecosystem: "nix"
|
|
66
|
+
directory: "/"
|
|
67
|
+
schedule:
|
|
68
|
+
interval: "monthly"
|
|
69
|
+
time: "06:00"
|
|
70
|
+
timezone: "UTC"
|
|
71
|
+
open-pull-requests-limit: 1
|
|
72
|
+
# `chore(deps): ...` -- the flake is the development shell, the checks and the Nix package,
|
|
73
|
+
# nothing RubyGems ships: a command's image takes the new inputs at its next release
|
|
74
|
+
commit-message:
|
|
75
|
+
prefix: "chore"
|
|
76
|
+
include: "scope"
|
|
77
|
+
labels:
|
|
78
|
+
- "dependencies"
|
|
79
|
+
- "nix"
|
|
80
|
+
groups:
|
|
81
|
+
flake-inputs:
|
|
82
|
+
patterns:
|
|
83
|
+
- "*"
|
|
84
|
+
cooldown:
|
|
85
|
+
default-days: 7
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The repositories a release's image goes to, one a line, each logged in before anything is
|
|
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,
|
|
6
|
+
# DOCKERHUB_TOKEN and DOCKERHUB_REPOSITORY (empty when unset).
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin <<< "${GH_TOKEN:?}" >&2
|
|
10
|
+
echo "ghcr.io/${GITHUB_REPOSITORY,,}"
|
|
11
|
+
|
|
12
|
+
username=${DOCKERHUB_USERNAME:-}
|
|
13
|
+
token=${DOCKERHUB_TOKEN:-}
|
|
14
|
+
if [[ -z $username && -z $token ]]; then
|
|
15
|
+
exit 0
|
|
16
|
+
elif [[ -z $username || -z $token ]]; then
|
|
17
|
+
echo "error: Docker Hub needs both vars.DOCKERHUB_USERNAME and secrets.DOCKERHUB_TOKEN" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
repository=${DOCKERHUB_REPOSITORY:-$username/${GITHUB_REPOSITORY#*/}}
|
|
21
|
+
repository=${repository,,}
|
|
22
|
+
# a reference Docker takes (its grammar's path components), its name as Docker Hub's: lowercase
|
|
23
|
+
# letters and digits, `-` and `_` between them, 2 to 255 characters (Docker Hub's docs)
|
|
24
|
+
name=${repository#*/}
|
|
25
|
+
if [[ ! $repository =~ ^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*/[a-z0-9]+((_|__|-+)[a-z0-9]+)*$ ]] ||
|
|
26
|
+
((${#name} < 2 || ${#name} > 255)); then
|
|
27
|
+
echo "error: '$repository' is no Docker Hub repository: set vars.DOCKERHUB_REPOSITORY" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
docker login docker.io --username "$username" --password-stdin <<< "$token" >&2
|
|
31
|
+
echo "docker.io/$repository"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# A release's notes: CHANGELOG.md's first section, up to the second `## ` heading, refused unless
|
|
3
|
+
# it is the version's own (`rake bump` writes it on top). The release workflow's.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
changelog=${1:?usage: release-notes.sh CHANGELOG VERSION}
|
|
7
|
+
version=${2:?usage: release-notes.sh CHANGELOG VERSION}
|
|
8
|
+
notes=$(awk '/^## /{n++} n==1' "$changelog")
|
|
9
|
+
if [[ $notes != "## [$version]("* ]]; then
|
|
10
|
+
echo "$changelog: the first section is not $version's" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
printf '%s\n' "$notes"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Dependabot Auto-Merge
|
|
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)
|
|
12
|
+
#
|
|
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.
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
pull_request:
|
|
27
|
+
|
|
28
|
+
permissions: {}
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
dependabot:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
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
|
|
40
|
+
# github.actor: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts
|
|
41
|
+
#
|
|
42
|
+
# To keep forks from auto-merging their own bumps, pin the repository:
|
|
43
|
+
# && github.repository == 'your-org/your-repo'
|
|
44
|
+
if: github.event.pull_request.user.login == 'dependabot[bot]'
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
- name: Fetch Dependabot metadata
|
|
48
|
+
id: metadata
|
|
49
|
+
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
|
50
|
+
with:
|
|
51
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
52
|
+
|
|
53
|
+
- 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).
|
|
62
|
+
if: >-
|
|
63
|
+
contains(
|
|
64
|
+
fromJSON('["version-update:semver-patch", "version-update:semver-minor"]'),
|
|
65
|
+
steps.metadata.outputs.update-type
|
|
66
|
+
)
|
|
67
|
+
run: gh pr merge --auto --squash "$PR_URL"
|
|
68
|
+
env:
|
|
69
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
70
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|