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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +53 -0
  3. data/LICENSE-APACHE +201 -0
  4. data/LICENSE-MIT +7 -0
  5. data/README.md +151 -0
  6. data/lib/veragem/generator.rb +109 -0
  7. data/lib/veragem/name.rb +111 -0
  8. data/lib/veragem/options.rb +53 -0
  9. data/lib/veragem/project.rb +77 -0
  10. data/lib/veragem/version.rb +5 -0
  11. data/lib/veragem.rb +23 -0
  12. data/plugins.rb +15 -0
  13. data/sig/bundler.rbs +10 -0
  14. data/sig/veragem.rbs +79 -0
  15. data/template/%name%.gemspec.erb +30 -0
  16. data/template/.editorconfig +15 -0
  17. data/template/.githooks/commit-msg +12 -0
  18. data/template/.githooks/pre-commit +6 -0
  19. data/template/.githooks/pre-push +17 -0
  20. data/template/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  21. data/template/.github/ISSUE_TEMPLATE/config.yml.erb +5 -0
  22. data/template/.github/ISSUE_TEMPLATE/documentation.md +19 -0
  23. data/template/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  24. data/template/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  25. data/template/.github/dependabot.yml +85 -0
  26. data/template/.github/image-registries.sh +31 -0
  27. data/template/.github/release-notes.sh +13 -0
  28. data/template/.github/workflows/dependabot-automerge.yml +70 -0
  29. data/template/.github/workflows/dependency-review.yml +50 -0
  30. data/template/.github/workflows/image.yml +140 -0
  31. data/template/.github/workflows/nix.yml +46 -0
  32. data/template/.github/workflows/release.yml +117 -0
  33. data/template/.github/workflows/rubocop-autocorrect.yml +114 -0
  34. data/template/.github/workflows/ruby-ci.yml +62 -0
  35. data/template/.github/workflows/security.yml +43 -0
  36. data/template/.github/workflows/style-check-cocogitto.yml +56 -0
  37. data/template/.github/workflows/style-check-dprint.yml +44 -0
  38. data/template/.gitignore +12 -0
  39. data/template/.rubocop.yml +27 -0
  40. data/template/AGENTS.md.erb +75 -0
  41. data/template/CODE_OF_CONDUCT.md.erb +18 -0
  42. data/template/CONTRIBUTING.md.erb +183 -0
  43. data/template/Gemfile +24 -0
  44. data/template/LICENSE-APACHE.erb +201 -0
  45. data/template/LICENSE-MIT.erb +7 -0
  46. data/template/README.md.erb +82 -0
  47. data/template/Rakefile +136 -0
  48. data/template/SECURITY.md.erb +21 -0
  49. data/template/Steepfile.erb +11 -0
  50. data/template/bin/console.erb +8 -0
  51. data/template/bin/setup +5 -0
  52. data/template/cliff.toml +70 -0
  53. data/template/cog.toml +8 -0
  54. data/template/dprint.json +23 -0
  55. data/template/exe/%name%.erb +6 -0
  56. data/template/flake.lock +27 -0
  57. data/template/flake.nix +213 -0
  58. data/template/lib/%path%/cli.rb.erb +45 -0
  59. data/template/lib/%path%/version.rb.erb +3 -0
  60. data/template/lib/%path%.rb.erb +13 -0
  61. data/template/sig/%path%.rbs.erb +16 -0
  62. data/template/test/test_%underscored%.rb.erb +27 -0
  63. data/template/test/test_cli.rb.erb +40 -0
  64. data/template/test/test_helper.rb.erb +15 -0
  65. data/template/typeprof.conf.jsonc +7 -0
  66. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Veragem
4
+ VERSION = "0.1.0"
5
+ 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,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Report a Security Vulnerability
4
+ url: https://github.com/<%= github %>/<%= name %>/security/advisories/new
5
+ about: Privately report a vulnerability. Do NOT use standard issues.
@@ -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 }}