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
data/template/Rakefile ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `rake` runs the Ruby checks, as ruby-ci.yml does; `rake checks` runs everything CI runs.
4
+ # `rake -T` lists the tasks.
5
+
6
+ require "bundler/audit/task"
7
+ require "bundler/gem_tasks"
8
+ require "json"
9
+ require "minitest/test_task"
10
+ require "rubocop/rake_task"
11
+ require "steep/rake_task"
12
+
13
+ Minitest::TestTask.create
14
+ RuboCop::RakeTask.new
15
+ Steep::RakeTask.new
16
+ Bundler::Audit::Task.new
17
+
18
+ task default: %i[rubocop steep test]
19
+
20
+ # TypeProf infers; Steep, against sig/, is the gate. TypeProf reads Ruby's core signatures alone,
21
+ # not the standard library's.
22
+ desc "Print the signatures TypeProf infers from lib/, to start sig/ from, and what it finds amiss"
23
+ task :typeprof do
24
+ sh "typeprof", "--show-errors", "lib"
25
+ end
26
+
27
+ desc "Update the advisory database, then check Gemfile.lock against it (bundler-audit)"
28
+ task audit: %w[bundle:audit:update bundle:audit:check]
29
+
30
+ desc "Check the Markdown and TOML format (dprint)"
31
+ task :dprint do
32
+ sh "dprint", "check"
33
+ end
34
+
35
+ desc "Check that every commit is a Conventional Commit (cocogitto)"
36
+ task :cog do
37
+ sh "cog", "check"
38
+ end
39
+
40
+ # A command's standard output; its failure fails the task, as `sh`'s does.
41
+ def output(*command)
42
+ IO.popen(command, &:read).tap { raise "#{command.join(" ")} failed" unless Process.last_status.success? }
43
+ end
44
+
45
+ desc "Lint and check the format of the shell scripts git tracks (shellcheck, shfmt)"
46
+ task :shell do
47
+ tracked = output("git", "ls-files", "-z").split("\0").select { File.file?(it) }
48
+ scripts = output("shfmt", "--find", *tracked).lines(chomp: true)
49
+ sh "shellcheck", *scripts
50
+ sh "shfmt", "--diff", *scripts
51
+ end
52
+
53
+ desc "Lint the workflows and audit their security (actionlint, zizmor)"
54
+ task :workflows do
55
+ sh "actionlint"
56
+ sh "zizmor", "--offline", "--quiet", "--strict-collection", "."
57
+ end
58
+
59
+ desc "Run the flake's checks: the Nix format, the shell, the workflows, the package (git's files alone)"
60
+ task :nix do
61
+ sh "nix", "flake", "check"
62
+ end
63
+
64
+ desc "Run every check CI runs"
65
+ task checks: %i[default audit dprint cog shell workflows nix]
66
+
67
+ desc "Format the Ruby, the Markdown and TOML, and the Nix (modifies files)"
68
+ task format: "rubocop:autocorrect" do
69
+ sh "dprint", "fmt"
70
+ sh "nix", "fmt"
71
+ end
72
+
73
+ desc "Use .githooks/: the quick checks before a commit, Conventional Commits for every message"
74
+ task :hooks do
75
+ sh "git", "config", "core.hooksPath", ".githooks"
76
+ end
77
+
78
+ gemspec = Bundler::GemHelper.gemspec
79
+ # git-cliff's compare links: the repository, owner/name
80
+ repository = gemspec.metadata.fetch("source_code_uri").delete_prefix("https://github.com/")
81
+ cliff = ["git-cliff", "--offline", "--github-repo", repository]
82
+
83
+ desc "Preview the changelog's next section"
84
+ task :changelog do
85
+ sh(*cliff, "--unreleased")
86
+ end
87
+
88
+ # 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.
90
+ def next_version(current, level)
91
+ abort "error: #{current} is not MAJOR.MINOR.PATCH" unless current.match?(/\A\d+\.\d+\.\d+\z/)
92
+ return current if output("git", "tag", "--list", "v[0-9]*").empty?
93
+
94
+ parts = current.split(".").map(&:to_i)
95
+ parts[level] += 1
96
+ parts.fill(0, level + 1).join(".")
97
+ end
98
+
99
+ # The release commit: the version, the lock, the changelog.
100
+ desc "Bump the version (major, minor or patch), write the changelog and commit"
101
+ task :bump, [:level] => "release:guard_clean" do |_, args|
102
+ level = %w[major minor patch].index(args[:level]) or abort 'usage: rake "bump[major|minor|patch]"'
103
+ current = gemspec.version.to_s
104
+ version = next_version(current, level)
105
+ tagged = system("git", "rev-parse", "--quiet", "--verify", "refs/tags/v#{version}", out: File::NULL)
106
+ abort "error: v#{version} is already tagged" if tagged
107
+ entries = JSON.parse(output(*cliff, "--unreleased", "--context")).sum { it["commits"].size }
108
+ abort "error: nothing to release: no commit since the last tag enters the changelog" if entries.zero?
109
+
110
+ file = "lib/#{gemspec.name.tr("-", "/")}/version.rb"
111
+ abort "error: #{file} declares no \"#{current}\"" unless File.read(file).include?(%("#{current}"))
112
+ first = !File.exist?("CHANGELOG.md")
113
+ begin
114
+ File.write(file, File.read(file).sub(%("#{current}"), %("#{version}")))
115
+ sh "bundle", "lock"
116
+ sh(*cliff, "--tag", "v#{version}", "--output", "CHANGELOG.md")
117
+ sh "git", "add", file, "Gemfile.lock", "CHANGELOG.md"
118
+ sh "git", "commit", "--message", "chore(release): v#{version}"
119
+ rescue StandardError, Interrupt
120
+ # The tracked files were committed (release:guard_clean): put back what this changed.
121
+ sh "git", "reset", "--hard", "--quiet"
122
+ rm_f "CHANGELOG.md" if first
123
+ raise
124
+ end
125
+ puts "v#{version} committed: `rake release` tags and pushes it, then release.yml publishes the tag."
126
+ end
127
+
128
+ # The gem is published by release.yml, on the tag, through RubyGems' Trusted Publishing:
129
+ # `rake release` builds it, tags v<version> and pushes the branch and the tag, and pushes no gem.
130
+ Rake::Task["release"].clear_comments
131
+ Rake::Task["release"].add_description("Tag v#{gemspec.version}, push the branch and the tag: release.yml publishes it")
132
+ Rake::Task["release:rubygem_push"].clear_actions
133
+ desc "Push no gem: release.yml publishes the tag"
134
+ task "release:rubygem_push" do
135
+ puts "release.yml publishes v#{gemspec.version} to RubyGems."
136
+ end
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security updates are applied to the latest `main` branch and the most recent release.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ **Do not open a public issue for security vulnerabilities.** To report one, use the
10
+ [Private Vulnerability Reporting](https://github.com/<%= github %>/<%= name %>/security/advisories/new)
11
+ feature in this repository.
12
+
13
+ Please include the technical context needed to reproduce it:
14
+
15
+ - The version or commit affected.
16
+ - The exact input, command line, or API call.
17
+ - Host OS and Ruby version.
18
+ - Exact reproduction steps or a proof-of-concept.
19
+
20
+ You can expect an initial response within a few days. Please give us a reasonable window to ship
21
+ a fix before disclosing publicly.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Steep checks lib/ against sig/, its strictest diagnostics on: `rake steep`.
4
+ target :lib do
5
+ signature "sig"
6
+ check "lib"
7
+ <%- if exe -%>
8
+ library "optparse"
9
+ <%- end -%>
10
+ configure_code_diagnostics(Steep::Diagnostic::Ruby.strict)
11
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "<%= path %>"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # The gems at Gemfile.lock's versions; the first run writes the lock, to commit.
5
+ system("bundle", "install", exception: true)
@@ -0,0 +1,70 @@
1
+ # git-cliff: CHANGELOG.md from the Conventional Commits, written by `rake bump` (never by hand).
2
+ # Grouped by commit type, breaking changes first; what changes nothing a user meets (chores, CI,
3
+ # formatting, the release commit itself) is left out.
4
+ #
5
+ # It names no project: the Rakefile passes the repository the headings' compare links need
6
+ # (`--github-repo`, from the gemspec's source_code_uri). Run bare, the headings carry no link.
7
+
8
+ [changelog]
9
+ header = """
10
+ # Changelog
11
+
12
+ All notable changes to this project are documented in this file, generated by
13
+ [git-cliff](https://git-cliff.org) from its commits; versions follow
14
+ [Semantic Versioning](https://semver.org).\n
15
+ """
16
+ body = """
17
+ {% set url = "" -%}
18
+ {% if remote.github.owner -%}
19
+ {% set url = "https://github.com/" ~ remote.github.owner ~ "/" ~ remote.github.repo -%}
20
+ {% endif -%}
21
+ {% if version -%}
22
+ {% set number = version | trim_start_matches(pat="v") -%}
23
+ ## {% if url %}[{{ number }}]({{ url }}/\
24
+ {%- if previous.version %}compare/{{ previous.version }}..{{ version }}\
25
+ {%- else %}tree/{{ version }}{% endif %})\
26
+ {%- else %}{{ number }}{% endif %} - \
27
+ {{ timestamp | date(format="%Y-%m-%d") }}
28
+ {% elif url and previous.version -%}
29
+ ## [Unreleased]({{ url }}/compare/{{ previous.version }}..HEAD)
30
+ {% else -%}
31
+ ## Unreleased
32
+ {% endif -%}
33
+ {% for group, commits in commits | group_by(attribute="group") %}
34
+ ### {{ group | striptags | trim }}
35
+ {% for commit in commits %}
36
+ - {% if commit.scope %}{{ commit.scope }}: {% endif %}\
37
+ {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
38
+ {% endfor %}
39
+ {% endfor %}\n
40
+ """
41
+ trim = true
42
+
43
+ [git]
44
+ conventional_commits = true
45
+ # a body right under its subject, which cog takes, set apart as git-cliff's parser needs it: its
46
+ # footers (BREAKING CHANGE) read
47
+ commit_preprocessors = [{ pattern = '\A([^\n]*)\n([^\n])', replace = "${1}\n\n${2}" }]
48
+ # what cog lets through unconventional -- a fixup (cog.toml) -- left out
49
+ filter_unconventional = true
50
+ protect_breaking_commits = true # a breaking change is listed whatever its type, first
51
+ # by type, in any case (cog takes `Feat:`), the HTML comments ordering the groups
52
+ commit_parsers = [
53
+ # a merge, whatever its message: cog checks none (ignore_merge_commits)
54
+ { field = "merge_commit", pattern = "true", skip = true },
55
+ { message = "^[a-zA-Z]+(\\([^)]*\\))?!:", group = "<!-- 0 -->Breaking changes" },
56
+ { footer = "^BREAKING[ -]CHANGE", group = "<!-- 0 -->Breaking changes" },
57
+ { message = "(?i)^(chore|ci|style)", skip = true },
58
+ { message = "(?i)^feat", group = "<!-- 1 -->Features" },
59
+ { message = "(?i)^fix", group = "<!-- 2 -->Fixes" },
60
+ { message = "(?i)^perf", group = "<!-- 3 -->Performance" },
61
+ { message = "(?i)^refactor", group = "<!-- 4 -->Refactoring" },
62
+ { message = "(?i)^build", group = "<!-- 5 -->Build" },
63
+ { message = "(?i)^doc", group = "<!-- 6 -->Documentation" },
64
+ { message = "(?i)^test", group = "<!-- 7 -->Tests" },
65
+ { message = "(?i)^revert", group = "<!-- 8 -->Reverts" },
66
+ { message = ".*", skip = true },
67
+ ]
68
+ sort_commits = "oldest"
69
+ # the release tags, `v0.1.0` on (Bundler's own form, `rake release`)
70
+ tag_pattern = "^v[0-9]"
data/template/cog.toml ADDED
@@ -0,0 +1,8 @@
1
+ # cocogitto checks the commits alone (`rake cog`, the commit-msg and pre-push hooks, CI):
2
+ # `rake bump` and `rake release` cut the release, git-cliff writes the changelog (cliff.toml).
3
+
4
+ # every commit, not from the latest tag: with no tag, cog stops "unable to get any tag"
5
+ from_latest_tag = false
6
+ ignore_merge_commits = true
7
+ ignore_fixup_commits = true
8
+ disable_changelog = true
@@ -0,0 +1,23 @@
1
+ // Markdown and TOML (`rake dprint`, style-check-dprint.yml). The plugins pinned:
2
+ // `dprint config update` moves them to their latest.
3
+ {
4
+ "markdown": {
5
+ "textWrap": "maintain",
6
+ // the plugin's defaults are underscores
7
+ "emphasisKind": "asterisks",
8
+ "strongKind": "asterisks"
9
+ },
10
+ // four spaces, as cliff.toml and cog.toml are written
11
+ "toml": { "indentWidth": 4 },
12
+ "excludes": [
13
+ // git-cliff's (cliff.toml): regenerated at each release, never edited
14
+ "CHANGELOG.md",
15
+ "coverage/",
16
+ "pkg/",
17
+ "vendor/"
18
+ ],
19
+ "plugins": [
20
+ "https://plugins.dprint.dev/markdown-0.26.0.wasm",
21
+ "https://plugins.dprint.dev/toml-0.9.0.wasm"
22
+ ]
23
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "<%= path %>/cli"
5
+
6
+ exit <%= constant %>::CLI.run(ARGV)
@@ -0,0 +1,27 @@
1
+ {
2
+ "nodes": {
3
+ "nixpkgs": {
4
+ "locked": {
5
+ "lastModified": 1792773339,
6
+ "narHash": "sha256-rZLrzrC6i+jnTAlvPZHogKDulY7WWDEv9hJeHieO7Zw=",
7
+ "owner": "NixOS",
8
+ "repo": "nixpkgs",
9
+ "rev": "3ec636362e1816bc2f0888fa0fa2c69f1bc9e504",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "NixOS",
14
+ "ref": "nixos-unstable",
15
+ "repo": "nixpkgs",
16
+ "type": "github"
17
+ }
18
+ },
19
+ "root": {
20
+ "inputs": {
21
+ "nixpkgs": "nixpkgs"
22
+ }
23
+ }
24
+ },
25
+ "root": "root",
26
+ "version": 7
27
+ }
@@ -0,0 +1,213 @@
1
+ # The development shell, the gem as a package, and the checks that need no gem from the bundle:
2
+ #
3
+ # nix develop Ruby 4.0 and every tool `rake checks` runs besides the gems (Bundler's, from
4
+ # Gemfile.lock: `bin/setup`): cocogitto, git-cliff, dprint, shellcheck, shfmt,
5
+ # actionlint, zizmor, nixfmt
6
+ # nix build the gem, built from its gemspec and installed (nixpkgs' buildRubyGem);
7
+ # with a command (exe/<name>): `nix run` runs it, and on Linux
8
+ # `nix build .#image` builds it as an OCI image (image.yml publishes it)
9
+ # nix fmt the Nix, by nixfmt
10
+ # nix flake check the Nix format, the shell scripts (shellcheck, shfmt), the workflows
11
+ # (actionlint, zizmor), the package and its library loaded -- and the command
12
+ # run, and its image -- offline in the sandbox: git's files alone
13
+ #
14
+ # Nothing here names the gem: its name is the gemspec's file; its version, lib/<path>/version.rb's
15
+ # VERSION; its summary, homepage and licences, the gemspec's lines that set them.
16
+ {
17
+ description = "A Ruby gem's development shell, package and checks";
18
+
19
+ # the unstable channel: its tools move with each `nix flake update` (Dependabot's, monthly)
20
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
21
+
22
+ outputs =
23
+ { self, nixpkgs }:
24
+ let
25
+ inherit (nixpkgs) lib;
26
+ # nixpkgs-unstable's platforms: 26.05 was the last release to support x86_64-darwin
27
+ systems = [
28
+ "x86_64-linux"
29
+ "aarch64-linux"
30
+ "aarch64-darwin"
31
+ ];
32
+ forAllSystems = f: lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
33
+
34
+ gemspecs = lib.filter (lib.hasSuffix ".gemspec") (lib.attrNames (builtins.readDir ./.));
35
+ name =
36
+ if lib.length gemspecs == 1 then
37
+ lib.removeSuffix ".gemspec" (lib.head gemspecs)
38
+ else
39
+ throw "flake.nix: one *.gemspec expected, found ${toString gemspecs}";
40
+ path = lib.replaceStrings [ "-" ] [ "/" ] name;
41
+ # the groups of the first of +file+'s lines that +pattern+ matches whole, or null
42
+ line =
43
+ pattern: file:
44
+ lib.findFirst (groups: groups != null) null (
45
+ map (builtins.match pattern) (lib.splitString "\n" (builtins.readFile file))
46
+ );
47
+ # a double-quoted Ruby string's value, its escapes \\, \" and \# read: those veragem writes
48
+ unquote = lib.replaceStrings [ "\\\\" "\\\"" "\\#" ] [ "\\" "\"" "#" ];
49
+ # a line's end after its value: `.freeze`, a comment, a carriage return
50
+ rest = ''(\.freeze)?[[:space:]]*(#.*)?'';
51
+ # the gemspec's `spec.<attribute> = "<value>"`, its value; "" if no line sets it so
52
+ field =
53
+ attribute:
54
+ let
55
+ found = line ''[[:space:]]*spec\.${attribute}[[:space:]]*=[[:space:]]*"((\\.|[^"\\])*)"${rest}'' ./${name}.gemspec;
56
+ in
57
+ if found == null then "" else unquote (lib.head found);
58
+ version =
59
+ let
60
+ found = line ''[[:space:]]*VERSION[[:space:]]*=[[:space:]]*"([^"]+)"${rest}'' ./lib/${path}/version.rb;
61
+ in
62
+ if found == null then
63
+ throw "flake.nix: no VERSION = \"...\" line in version.rb"
64
+ else
65
+ lib.head found;
66
+ # the SPDX identifiers of `spec.licenses = [...]` or `spec.license = "..."`
67
+ licences =
68
+ let
69
+ found = line ''[[:space:]]*spec\.licenses?[[:space:]]*=[[:space:]]*(\[[^]]*]|"[^"]*")${rest}'' ./${name}.gemspec;
70
+ in
71
+ if found == null then
72
+ [ ]
73
+ else
74
+ lib.concatLists (lib.filter lib.isList (builtins.split ''"([^"]*)"'' (lib.head found)));
75
+ command = builtins.pathExists ./exe/${name};
76
+
77
+ check =
78
+ pkgs: checkName: tools: script:
79
+ pkgs.runCommand checkName { nativeBuildInputs = tools; } ''
80
+ cd ${self}
81
+ ${script}
82
+ touch $out
83
+ '';
84
+
85
+ perSystem =
86
+ pkgs:
87
+ let
88
+ ruby = pkgs.ruby_4_0;
89
+ # the gem's runtime dependencies (its gemspec's add_dependency), as nixpkgs packages
90
+ # them: pkgs.rubyPackages_4_0.<gem>. The `library` check fails on one missing
91
+ dependencies = [ ];
92
+ package = pkgs.buildRubyGem {
93
+ gemName = name;
94
+ inherit version ruby;
95
+ src = self;
96
+ gemPath = dependencies;
97
+ # no meta.license: nixpkgs refuses to evaluate a package whose licence is unfree or unknown
98
+ # to it, and the gemspec's is the author's to choose
99
+ meta = {
100
+ description = field "summary";
101
+ homepage = field "homepage";
102
+ };
103
+ };
104
+ # the command as an OCI image: its closure alone -- no base image, no /bin/sh -- run as
105
+ # nobody. `.#image.stream` streams the archive uncompressed (image.yml's: no second copy
106
+ # of the closure in the store)
107
+ image = pkgs.dockerTools.buildLayeredImage {
108
+ inherit name;
109
+ tag = version;
110
+ # Dir.tmpdir's default, and HOME's: the image has no other directory to write
111
+ extraCommands = "mkdir -m 1777 tmp";
112
+ config = {
113
+ Entrypoint = [ (lib.getExe package) ];
114
+ # nobody: nothing in it needs root, and a numeric user needs no /etc/passwd
115
+ User = "65534:65534";
116
+ Env = [
117
+ # UTF-8 text, ARGV and files alike (glibc's built-in locale)
118
+ "LANG=C.UTF-8"
119
+ "HOME=/tmp"
120
+ # what OpenSSL verifies HTTPS against: the image has no /etc/ssl
121
+ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
122
+ ];
123
+ # what GHCR reads (docs.github.com, "Working with the Container registry"): the
124
+ # source links the image to its repository
125
+ Labels = lib.filterAttrs (_: value: value != "") {
126
+ "org.opencontainers.image.title" = name;
127
+ "org.opencontainers.image.description" = field "summary";
128
+ "org.opencontainers.image.version" = version;
129
+ "org.opencontainers.image.source" = field "homepage";
130
+ "org.opencontainers.image.licenses" = lib.concatStringsSep " OR " licences;
131
+ };
132
+ };
133
+ };
134
+ linuxCommand = command && pkgs.stdenv.hostPlatform.isLinux;
135
+ in
136
+ {
137
+ packages = {
138
+ default = package;
139
+ }
140
+ // lib.optionalAttrs linuxCommand { inherit image; };
141
+
142
+ checks = {
143
+ format = check pkgs "format" [ pkgs.nixfmt ] "find . -name '*.nix' -exec nixfmt --check {} +";
144
+ # shfmt finds the scripts, by extension or shebang, and formats them as .editorconfig says
145
+ shell = check pkgs "shell" [ pkgs.shellcheck pkgs.shfmt ] ''
146
+ shfmt --find . | xargs shellcheck
147
+ shfmt --diff .
148
+ '';
149
+ # actionlint runs shellcheck on each `run:` script too; zizmor audits Dependabot's
150
+ # configuration as well
151
+ workflows = check pkgs "workflows" [ pkgs.actionlint pkgs.shellcheck pkgs.zizmor ] ''
152
+ actionlint .github/workflows/*.yml
153
+ zizmor --offline --quiet --strict-collection .
154
+ '';
155
+ inherit package;
156
+ # the library as a user loads it: the gem activated -- its dependencies with it -- then
157
+ # required, in an environment of nothing else (env -i). Ruby's setup hook puts each gem of
158
+ # buildInputs on GEM_PATH, those it propagates too: the dependencies' own dependencies
159
+ library =
160
+ pkgs.runCommand "library"
161
+ {
162
+ buildInputs = [
163
+ ruby
164
+ package
165
+ ];
166
+ }
167
+ ''
168
+ env -i GEM_PATH="$GEM_PATH" ${lib.getExe ruby} -e 'gem "${name}"; require "${path}"'
169
+ touch $out
170
+ '';
171
+ }
172
+ // lib.optionalAttrs command {
173
+ # the command as a user runs it: its wrapper alone gives it its environment (env -i)
174
+ command =
175
+ pkgs.runCommand "command"
176
+ {
177
+ exe = lib.getExe package;
178
+ expected = "${name} ${version}";
179
+ }
180
+ ''
181
+ actual=$(env -i "$exe" --version)
182
+ if [ "$actual" != "$expected" ]; then
183
+ echo "--version gave '$actual', expected '$expected'" >&2
184
+ exit 1
185
+ fi
186
+ touch $out
187
+ '';
188
+ }
189
+ // lib.optionalAttrs linuxCommand { inherit image; };
190
+
191
+ devShells.default = pkgs.mkShell {
192
+ packages = [
193
+ ruby
194
+ pkgs.cocogitto
195
+ pkgs.git-cliff
196
+ pkgs.dprint
197
+ pkgs.shellcheck
198
+ pkgs.shfmt
199
+ pkgs.actionlint
200
+ pkgs.zizmor
201
+ pkgs.nixfmt
202
+ ];
203
+ };
204
+ };
205
+ output = attribute: forAllSystems (pkgs: (perSystem pkgs).${attribute});
206
+ in
207
+ {
208
+ packages = output "packages";
209
+ checks = output "checks";
210
+ devShells = output "devShells";
211
+ formatter = forAllSystems (pkgs: pkgs.nixfmt-tree);
212
+ };
213
+ }
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../<%= File.basename(path) %>"
5
+
6
+ <%= nest(<<~RUBY)
7
+ # The command line: `#{name} [NAME]` prints the greeting.
8
+ module CLI
9
+ # Runs the command line with +argv+; returns the exit status.
10
+ def self.run(argv)
11
+ options = { help: false, version: false }
12
+ names = parser.parse(argv, into: options)
13
+ puts output(options, names.first || "")
14
+ 0
15
+ rescue ::OptionParser::ParseError => e
16
+ warn e.message, parser.help
17
+ 2
18
+ end
19
+
20
+ # What +options+ ask for, the help or the version, else the greeting for +name+.
21
+ def self.output(options, name)
22
+ if options[:help]
23
+ parser.help
24
+ elsif options[:version]
25
+ parser.ver
26
+ else
27
+ #{modules.last}.greeting(name)
28
+ end
29
+ end
30
+ private_class_method :output
31
+
32
+ # --help as an option of its own: OptionParser's built-in one exits the process. `::`: a module of
33
+ # the gem's could be named OptionParser
34
+ def self.parser
35
+ ::OptionParser.new("Usage: #{name} [options] [NAME]") do |opts|
36
+ opts.program_name = "#{name}"
37
+ opts.version = VERSION
38
+ opts.on("-h", "--help", "Print this help")
39
+ opts.on("--version", "Print the version")
40
+ end
41
+ end
42
+ private_class_method :parser
43
+ end
44
+ RUBY
45
+ -%>
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%= nest(%(VERSION = "0.1.0"\n)) -%>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "<%= File.basename(path) %>/version"
4
+
5
+ <%= nest(<<~RUBY, doc: summary)
6
+ # A greeting for +name+, its surrounding whitespace stripped; nobody named, the world.
7
+ def self.greeting(name)
8
+ who = name.strip
9
+ who = "world" if who.empty?
10
+ "Hello, \#{who}!"
11
+ end
12
+ RUBY
13
+ -%>
@@ -0,0 +1,16 @@
1
+ <%# `::`: a module of the gem's could be named String, or Hash -%>
2
+ <%= nest(<<~RBS + (exe ? <<~CLI : ""))
3
+ VERSION: ::String
4
+
5
+ def self.greeting: (::String name) -> ::String
6
+ RBS
7
+
8
+ module CLI
9
+ def self.run: (::Array[::String] argv) -> ::Integer
10
+
11
+ private def self.output: (::Hash[::Symbol, bool] options, ::String name) -> ::String?
12
+
13
+ private def self.parser: () -> ::OptionParser
14
+ end
15
+ CLI
16
+ -%>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Test<%= constant.delete(":") %> < Minitest::Test
6
+ def test_the_version_is_major_minor_patch
7
+ assert_match(/\A\d+\.\d+\.\d+\z/, <%= constant %>::VERSION)
8
+ end
9
+
10
+ def test_it_greets_the_name_given
11
+ assert_equal "Hello, Ada!", <%= constant %>.greeting("Ada")
12
+ end
13
+
14
+ def test_nobody_named_it_greets_the_world
15
+ assert_equal "Hello, world!", <%= constant %>.greeting(" \n")
16
+ end
17
+
18
+ # A property, checked on many generated names (pbt): surrounding whitespace changes nothing.
19
+ def test_surrounding_whitespace_changes_nothing
20
+ gem = <%= constant %>
21
+ Pbt.assert do
22
+ Pbt.property(Pbt.printable_string) do |name|
23
+ assert_equal gem.greeting(name), gem.greeting(" #{name}\n")
24
+ end
25
+ end
26
+ end
27
+ end