inquirex-tools 0.8.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/.DS_Store +0 -0
- data/.github_changelog_generator +5 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +39 -0
- data/exe/inquirex +29 -0
- data/lib/inquirex/tools/changelogs.rb +97 -0
- data/lib/inquirex/tools/commands/changelogs.rb +33 -0
- data/lib/inquirex/tools/commands/version.rb +16 -0
- data/lib/inquirex/tools/commands/versions.rb +41 -0
- data/lib/inquirex/tools/commands.rb +24 -0
- data/lib/inquirex/tools/version.rb +7 -0
- data/lib/inquirex/tools/versions.rb +125 -0
- data/lib/inquirex/tools/workspace.rb +150 -0
- data/lib/inquirex/tools.rb +30 -0
- data/sig/inquirex/tools.rbs +6 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2027915b1b69d07af80a30a63207731809243ae607da736ea2d558c2748c6c1f
|
|
4
|
+
data.tar.gz: 6c35d199ad702ffd061d90bf5fa5ebe17da1fc8b1f0d7722e3f8bf37676b3e4b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a97efe3bc05fc24445e9b40920ec073055305e26c92764a7977cea0152cd33a25c28ccca97cf3e129ced24150b5957f0a137c57bc10172c1054cea7a5300ddfb
|
|
7
|
+
data.tar.gz: 0a975ec1e72ad597bf19827d9fe5f5afe68523bfea5809729a23ae184ab61a380b7aeff9128df1e984042c307a9de6c4ea464c660bf668474e731aa78779664e
|
data/.DS_Store
ADDED
|
Binary file
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Gredeskoul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Inquirex::Tools
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/inquirex/tools`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/inquirex-tools.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
def shell(*args)
|
|
9
|
+
puts "running: #{args.join(" ")}"
|
|
10
|
+
system(args.join(" "))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task :clean do
|
|
14
|
+
shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task gem: [:build] do
|
|
18
|
+
shell("gem install pkg/*")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
task permissions: [:clean] do
|
|
22
|
+
# One traversal replaces a six-level glob chain that printed "No such file
|
|
23
|
+
# or directory" for every level this project does not have, skipped dotfiles
|
|
24
|
+
# entirely, and silently stopped at depth six. .git is pruned — its objects
|
|
25
|
+
# have no business being group-readable.
|
|
26
|
+
shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task build: :permissions
|
|
30
|
+
|
|
31
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
32
|
+
t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md]
|
|
33
|
+
t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
|
|
34
|
+
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
38
|
+
|
|
39
|
+
task default: :spec
|
data/exe/inquirex
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The `inquirex` umbrella CLI. `inquirex-tty` ships its own `inquirex-tty`
|
|
5
|
+
# binary; this gem owns the bare name so cross-family tooling has one entry
|
|
6
|
+
# point and no two gems install a binary called `inquirex`.
|
|
7
|
+
#
|
|
8
|
+
# inquirex versions # lockstep parity across the family
|
|
9
|
+
# inquirex versions --set 0.8.0
|
|
10
|
+
# inquirex changelogs
|
|
11
|
+
#
|
|
12
|
+
# Inside a checkout, prefer the repo's own Gemfile over whatever rubygems has
|
|
13
|
+
# installed globally — the same bootstrap inquirex-tty uses.
|
|
14
|
+
gemfile = File.expand_path("../Gemfile", __dir__)
|
|
15
|
+
if File.file?(gemfile)
|
|
16
|
+
ENV["BUNDLE_GEMFILE"] = gemfile
|
|
17
|
+
begin
|
|
18
|
+
require "bundler/setup"
|
|
19
|
+
rescue LoadError
|
|
20
|
+
# Bundler unavailable — fall through to priming $LOAD_PATH directly.
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) if File.directory?("#{__dir__}/../lib")
|
|
25
|
+
|
|
26
|
+
require "inquirex/tools"
|
|
27
|
+
require "inquirex/tools/commands"
|
|
28
|
+
|
|
29
|
+
Dry::CLI.new(Inquirex::Tools::Commands).call
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "workspace"
|
|
4
|
+
|
|
5
|
+
module Inquirex
|
|
6
|
+
module Tools
|
|
7
|
+
# Regenerates CHANGELOG.md across the ecosystem from merged GitHub PRs.
|
|
8
|
+
#
|
|
9
|
+
# The generated files are disposable. `inquirex/INQUIREX_VERSION_LOG.md` is
|
|
10
|
+
# not, and is never touched here — a PR title records what merged, not what
|
|
11
|
+
# it means or what a consumer must do about it.
|
|
12
|
+
class Changelogs
|
|
13
|
+
# The gem is not installed for every Ruby the repos pin (inquirex pins
|
|
14
|
+
# 4.0.5), so the generator runs under one that has it rather than being
|
|
15
|
+
# installed into each.
|
|
16
|
+
DEFAULT_RUBY = "4.0.6"
|
|
17
|
+
|
|
18
|
+
# Never overwritten by generation, in any repo.
|
|
19
|
+
PROTECTED = %w[INQUIREX_VERSION_LOG.md].freeze
|
|
20
|
+
|
|
21
|
+
# @param workspace [Workspace]
|
|
22
|
+
# @param out [IO]
|
|
23
|
+
def initialize(workspace: Workspace.new, out: $stdout)
|
|
24
|
+
@workspace = workspace
|
|
25
|
+
@out = out
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Array<String>] every repo carrying a generator config
|
|
29
|
+
def repositories
|
|
30
|
+
(Workspace::LOCKSTEP.keys + Workspace::CHANGELOG_ONLY).uniq.select do |name|
|
|
31
|
+
File.exist?(File.join(@workspace.root, name, ".github_changelog_generator"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Runs the generator in each repository.
|
|
36
|
+
#
|
|
37
|
+
# The token comes from `gh auth token` rather than a stored secret, so
|
|
38
|
+
# nothing here handles a credential in plain text and there is none to
|
|
39
|
+
# leak from the source.
|
|
40
|
+
#
|
|
41
|
+
# @param names [Array<String>] repositories, or all configured when empty
|
|
42
|
+
# @return [Boolean] true when every repository succeeded
|
|
43
|
+
def generate(*names)
|
|
44
|
+
token = `gh auth token 2>/dev/null`.strip
|
|
45
|
+
if token.empty?
|
|
46
|
+
@out.puts "ERROR: `gh auth token` returned nothing — run `gh auth login`"
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
targets = names.flatten.compact
|
|
51
|
+
targets = repositories if targets.empty?
|
|
52
|
+
failed = targets.reject { |name| generate_one(name, token) }
|
|
53
|
+
|
|
54
|
+
report_failures(failed)
|
|
55
|
+
failed.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# @param name [String] repository directory name
|
|
61
|
+
# @param token [String] GitHub token for the generator
|
|
62
|
+
# @return [Boolean] true on success or a deliberate skip
|
|
63
|
+
def generate_one(name, token)
|
|
64
|
+
dir = File.join(@workspace.root, name)
|
|
65
|
+
unless File.exist?(File.join(dir, ".github_changelog_generator"))
|
|
66
|
+
@out.puts format(" %<name>-16s SKIP (no .github_changelog_generator)", name: name)
|
|
67
|
+
return true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@out.print format(" %<name>-16s ", name: name)
|
|
71
|
+
env = { "CHANGELOG_GITHUB_TOKEN" => token, "RBENV_VERSION" => ENV.fetch("RBENV_VERSION", DEFAULT_RUBY) }
|
|
72
|
+
if system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
|
|
73
|
+
@out.puts "ok (#{line_count(File.join(dir, "CHANGELOG.md"))} lines)"
|
|
74
|
+
true
|
|
75
|
+
else
|
|
76
|
+
@out.puts "FAILED"
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param path [String]
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
def line_count(path) = File.exist?(path) ? File.readlines(path).size : 0
|
|
84
|
+
|
|
85
|
+
# @param failed [Array<String>]
|
|
86
|
+
# @return [void]
|
|
87
|
+
def report_failures(failed)
|
|
88
|
+
return if failed.empty?
|
|
89
|
+
|
|
90
|
+
@out.puts
|
|
91
|
+
@out.puts "Failed: #{failed.join(", ")}"
|
|
92
|
+
@out.puts "Re-run one with output: cd <repo> && " \
|
|
93
|
+
"CHANGELOG_GITHUB_TOKEN=$(gh auth token) github_changelog_generator"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inquirex
|
|
4
|
+
module Tools
|
|
5
|
+
module Commands
|
|
6
|
+
# Regenerates CHANGELOG.md across the ecosystem from merged GitHub PRs.
|
|
7
|
+
class Changelogs < Dry::CLI::Command
|
|
8
|
+
desc "Regenerate CHANGELOG.md from merged PRs"
|
|
9
|
+
|
|
10
|
+
argument :repos, type: :array, required: false,
|
|
11
|
+
desc: "Repositories to regenerate (default: all configured)"
|
|
12
|
+
|
|
13
|
+
example [
|
|
14
|
+
" # every repo with a .github_changelog_generator",
|
|
15
|
+
"inquirex-js # just one"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# The GitHub token comes from `gh auth token`, so no secret is stored
|
|
19
|
+
# or passed on the command line. inquirex/INQUIREX_VERSION_LOG.md is
|
|
20
|
+
# hand-written and never touched.
|
|
21
|
+
#
|
|
22
|
+
# @param repos [Array<String>, nil]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def call(repos: [], **)
|
|
25
|
+
exit(1) unless Inquirex::Tools::Changelogs.new.generate(*Array(repos))
|
|
26
|
+
rescue Inquirex::Tools::Error => e
|
|
27
|
+
warn "ERROR: #{e.message}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inquirex
|
|
4
|
+
module Tools
|
|
5
|
+
module Commands
|
|
6
|
+
# Prints this tool's own version. See `versions` (plural) for the
|
|
7
|
+
# family's lockstep version.
|
|
8
|
+
class Version < Dry::CLI::Command
|
|
9
|
+
desc "Print inquirex-tools version"
|
|
10
|
+
|
|
11
|
+
# @return [void]
|
|
12
|
+
def call(**) = puts("inquirex-tools #{Inquirex::Tools::VERSION}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inquirex
|
|
4
|
+
module Tools
|
|
5
|
+
module Commands
|
|
6
|
+
# Reports and enforces version parity across the lockstep packages.
|
|
7
|
+
#
|
|
8
|
+
# Plural on purpose: `inquirex version` prints this tool's own version,
|
|
9
|
+
# `inquirex versions` manages the family's.
|
|
10
|
+
class Versions < Dry::CLI::Command
|
|
11
|
+
desc "Check, set, or preflight the family's lockstep version"
|
|
12
|
+
|
|
13
|
+
option :set, type: :string, desc: "Move every lockstep package to this version"
|
|
14
|
+
option :preflight, type: :boolean, default: false,
|
|
15
|
+
desc: "Also check clean trees, main branch, and tags"
|
|
16
|
+
|
|
17
|
+
example [
|
|
18
|
+
" # report parity, exit 1 on drift",
|
|
19
|
+
"--set 0.8.0 # move all five packages",
|
|
20
|
+
"--preflight # release readiness"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# @param set [String, nil] target version
|
|
24
|
+
# @param preflight [Boolean]
|
|
25
|
+
# @return [void]
|
|
26
|
+
def call(set: nil, preflight: false, **)
|
|
27
|
+
versions = Inquirex::Tools::Versions.new
|
|
28
|
+
ok =
|
|
29
|
+
if set then versions.set(set)
|
|
30
|
+
elsif preflight then versions.preflight
|
|
31
|
+
else versions.check
|
|
32
|
+
end
|
|
33
|
+
exit(1) unless ok
|
|
34
|
+
rescue Inquirex::Tools::Error => e
|
|
35
|
+
warn "ERROR: #{e.message}"
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
|
|
5
|
+
require_relative "commands/version"
|
|
6
|
+
require_relative "commands/versions"
|
|
7
|
+
require_relative "commands/changelogs"
|
|
8
|
+
|
|
9
|
+
module Inquirex
|
|
10
|
+
module Tools
|
|
11
|
+
# dry-cli registry for the `inquirex` executable.
|
|
12
|
+
#
|
|
13
|
+
# `inquirex-tty` owns `inquirex-tty`; this gem owns the bare `inquirex`
|
|
14
|
+
# command, so cross-family tooling has one entry point and the two gems no
|
|
15
|
+
# longer install binaries with the same name.
|
|
16
|
+
module Commands
|
|
17
|
+
extend Dry::CLI::Registry
|
|
18
|
+
|
|
19
|
+
register "versions", Versions
|
|
20
|
+
register "changelogs", Changelogs
|
|
21
|
+
register "version", Version
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems/version"
|
|
4
|
+
require_relative "workspace"
|
|
5
|
+
|
|
6
|
+
module Inquirex
|
|
7
|
+
module Tools
|
|
8
|
+
# Reports and enforces version parity across the lockstep packages.
|
|
9
|
+
#
|
|
10
|
+
# @example Check for drift
|
|
11
|
+
# Inquirex::Tools::Versions.new.check # => false when versions differ
|
|
12
|
+
class Versions
|
|
13
|
+
# @param workspace [Workspace]
|
|
14
|
+
# @param out [IO] where report output goes
|
|
15
|
+
def initialize(workspace: Workspace.new, out: $stdout)
|
|
16
|
+
@workspace = workspace
|
|
17
|
+
@out = out
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Prints the current state of every lockstep package.
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean] true when all packages agree on one version
|
|
23
|
+
def check
|
|
24
|
+
versions = @workspace.versions
|
|
25
|
+
print_table(versions)
|
|
26
|
+
verdict(versions)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Moves every lockstep package to one version.
|
|
30
|
+
#
|
|
31
|
+
# Refuses to move backwards: a version already published cannot be
|
|
32
|
+
# reissued, so the target must exceed every current value.
|
|
33
|
+
#
|
|
34
|
+
# @param version [String] target, MAJOR.MINOR.PATCH
|
|
35
|
+
# @return [Boolean] true when written
|
|
36
|
+
def set(version)
|
|
37
|
+
unless /\A\d+\.\d+\.\d+\z/.match?(version.to_s)
|
|
38
|
+
@out.puts "ERROR: #{version.inspect} is not MAJOR.MINOR.PATCH"
|
|
39
|
+
return false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
current = @workspace.versions.values.compact
|
|
43
|
+
highest = current.max_by { Gem::Version.new(_1) }
|
|
44
|
+
if highest && Gem::Version.new(highest) > Gem::Version.new(version)
|
|
45
|
+
@out.puts "ERROR: #{version} is below the highest existing version (#{highest}) — versions only go forward"
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@out.puts "Setting all lockstep packages to #{version}:"
|
|
50
|
+
@workspace.versions.each do |name, was|
|
|
51
|
+
@workspace.write_version(name, version)
|
|
52
|
+
@out.puts format(" %-16s %s -> %s", name, was || "?", version)
|
|
53
|
+
end
|
|
54
|
+
@out.puts
|
|
55
|
+
@out.puts "Unchanged (excluded): #{Workspace::EXCLUDED.join(", ")}"
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parity plus git readiness: clean tree, on main, tagged at the version.
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean] true when every package could be published now
|
|
62
|
+
def preflight
|
|
63
|
+
ok = check
|
|
64
|
+
target = @workspace.versions.values.compact.uniq.first
|
|
65
|
+
@out.puts
|
|
66
|
+
@out.puts "Release readiness:"
|
|
67
|
+
|
|
68
|
+
Workspace::LOCKSTEP.each_key do |name|
|
|
69
|
+
problems = readiness_problems(name, target)
|
|
70
|
+
@out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready" : problems.join(", "))
|
|
71
|
+
ok &&= problems.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@out.puts
|
|
75
|
+
@out.puts ok ? "ALL READY — publish with `gem push` / `npm publish`" : "NOT READY — resolve the above"
|
|
76
|
+
ok
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# @param versions [Hash{String => String, nil}]
|
|
82
|
+
# @return [void]
|
|
83
|
+
def print_table(versions)
|
|
84
|
+
@out.puts "Lockstep packages (#{Workspace::EXCLUDED.join(", ")} excluded by design):"
|
|
85
|
+
versions.each do |name, version|
|
|
86
|
+
@out.puts format(" %<name>-16s %<kind>-4s %<version>s",
|
|
87
|
+
name: name, kind: Workspace::LOCKSTEP[name], version: version || "MISSING")
|
|
88
|
+
end
|
|
89
|
+
@out.puts
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param versions [Hash{String => String, nil}]
|
|
93
|
+
# @return [Boolean] true when every package agrees on one version
|
|
94
|
+
def verdict(versions)
|
|
95
|
+
distinct = versions.values.compact.uniq
|
|
96
|
+
if versions.value?(nil)
|
|
97
|
+
@out.puts "DRIFT: a version could not be read (MISSING above)"
|
|
98
|
+
false
|
|
99
|
+
elsif distinct.one?
|
|
100
|
+
@out.puts "OK: all #{versions.size} at #{distinct.first}"
|
|
101
|
+
true
|
|
102
|
+
else
|
|
103
|
+
@out.puts "DRIFT: #{distinct.sort.join(", ")} — run `inquirex versions --set <version>`"
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Why a package is not publishable right now.
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] package name
|
|
111
|
+
# @param target [String, nil] the version its tag must match
|
|
112
|
+
# @return [Array<String>] empty when ready
|
|
113
|
+
def readiness_problems(name, target)
|
|
114
|
+
state = @workspace.git_state(name)
|
|
115
|
+
return [ "no git checkout" ] if state[:clean].nil?
|
|
116
|
+
|
|
117
|
+
problems = []
|
|
118
|
+
problems << "DIRTY" unless state[:clean]
|
|
119
|
+
problems << "on #{state[:branch]}" unless state[:branch] == "main"
|
|
120
|
+
problems << "untagged" unless state[:tags].any? { |tag| tag.delete_prefix("v") == target }
|
|
121
|
+
problems
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Inquirex
|
|
6
|
+
module Tools
|
|
7
|
+
# Locates the Inquirex ecosystem checkout and reads each package's version.
|
|
8
|
+
#
|
|
9
|
+
# As gem executables these tools run from an rbenv shim, so `__dir__` says
|
|
10
|
+
# nothing about where the repositories live. The root is discovered by
|
|
11
|
+
# walking up from the working directory looking for the sibling checkouts,
|
|
12
|
+
# or taken from INQUIREX_ROOT.
|
|
13
|
+
class Workspace
|
|
14
|
+
# Packages that must always carry the same version. They share one
|
|
15
|
+
# serialization format — the gem defines the DSL and emits step JSON,
|
|
16
|
+
# inquirex-js renders it, inquirex-webui prints it back to DSL,
|
|
17
|
+
# inquirex-llm extends the vocabulary, and inquirex-tools is the tooling
|
|
18
|
+
# that ships alongside them. A verb present in one and absent in another
|
|
19
|
+
# does not fail loudly; it drops data.
|
|
20
|
+
#
|
|
21
|
+
# `inquirex-tty` is deliberately absent: a developer tool outside the
|
|
22
|
+
# wire contract, versioned on its own cadence.
|
|
23
|
+
LOCKSTEP = {
|
|
24
|
+
"inquirex" => :gem,
|
|
25
|
+
"inquirex-llm" => :gem,
|
|
26
|
+
"inquirex-tools" => :gem,
|
|
27
|
+
"inquirex-js" => :npm,
|
|
28
|
+
"inquirex-webui" => :npm
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# Excluded from lockstep, documented so the omission reads as deliberate.
|
|
32
|
+
EXCLUDED = %w[inquirex-tty].freeze
|
|
33
|
+
|
|
34
|
+
# Repositories that carry a generated CHANGELOG.md but are not versioned
|
|
35
|
+
# in lockstep.
|
|
36
|
+
CHANGELOG_ONLY = %w[inquirex-tty qualified-at].freeze
|
|
37
|
+
|
|
38
|
+
GEM_VERSION_RE = /VERSION\s*=\s*"([^"]+)"/
|
|
39
|
+
|
|
40
|
+
# Directories whose presence identifies the ecosystem root.
|
|
41
|
+
MARKERS = %w[inquirex inquirex-js].freeze
|
|
42
|
+
|
|
43
|
+
# @return [String] absolute path to the ecosystem root
|
|
44
|
+
attr_reader :root
|
|
45
|
+
|
|
46
|
+
# @param root [String, nil] explicit root; discovered when nil
|
|
47
|
+
# @raise [Error] when no root can be found
|
|
48
|
+
def initialize(root: nil)
|
|
49
|
+
@root = root || ENV["INQUIREX_ROOT"] || self.class.discover
|
|
50
|
+
raise Error, "cannot find the Inquirex workspace — run inside it or set INQUIREX_ROOT" if @root.nil?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Walks up from the working directory looking for a directory that holds
|
|
54
|
+
# the marker checkouts.
|
|
55
|
+
#
|
|
56
|
+
# @return [String, nil]
|
|
57
|
+
def self.discover
|
|
58
|
+
dir = Dir.pwd
|
|
59
|
+
loop do
|
|
60
|
+
return dir if MARKERS.all? { |m| Dir.exist?(File.join(dir, m)) }
|
|
61
|
+
|
|
62
|
+
parent = File.dirname(dir)
|
|
63
|
+
return nil if parent == dir
|
|
64
|
+
|
|
65
|
+
dir = parent
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param name [String] package name
|
|
70
|
+
# @return [String, nil] path to the file carrying its version
|
|
71
|
+
def version_file(name)
|
|
72
|
+
case LOCKSTEP[name]
|
|
73
|
+
when :npm then existing(File.join(name, "package.json"))
|
|
74
|
+
when :gem then gem_version_file(name)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param name [String] package name
|
|
79
|
+
# @return [String, nil] the current version, or nil when unreadable
|
|
80
|
+
def version_of(name)
|
|
81
|
+
file = version_file(name)
|
|
82
|
+
return nil unless file && File.exist?(file)
|
|
83
|
+
|
|
84
|
+
LOCKSTEP[name] == :gem ? File.read(file)[GEM_VERSION_RE, 1] : JSON.parse(File.read(file))["version"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Hash{String => String, nil}] every lockstep package's version
|
|
88
|
+
def versions = LOCKSTEP.keys.to_h { |n| [n, version_of(n)] }
|
|
89
|
+
|
|
90
|
+
# Rewrites one package's version. Edits the single version line rather
|
|
91
|
+
# than re-emitting the file, so formatting and comments survive.
|
|
92
|
+
#
|
|
93
|
+
# @param name [String] package name
|
|
94
|
+
# @param version [String] new version
|
|
95
|
+
# @return [void]
|
|
96
|
+
def write_version(name, version)
|
|
97
|
+
file = version_file(name)
|
|
98
|
+
raise Error, "no version file for #{name}" unless file
|
|
99
|
+
|
|
100
|
+
src = File.read(file)
|
|
101
|
+
updated =
|
|
102
|
+
if LOCKSTEP[name] == :gem
|
|
103
|
+
src.sub(GEM_VERSION_RE, %(VERSION = "#{version}"))
|
|
104
|
+
else
|
|
105
|
+
src.sub(/"version":\s*"[^"]*"/, %("version": "#{version}"))
|
|
106
|
+
end
|
|
107
|
+
File.write(file, updated)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param name [String] repository directory name
|
|
111
|
+
# @return [Hash] :clean, :branch, :tags — all nil when not a checkout
|
|
112
|
+
def git_state(name)
|
|
113
|
+
dir = File.join(@root, name)
|
|
114
|
+
return { clean: nil, branch: nil, tags: [] } unless File.exist?(File.join(dir, ".git"))
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
clean: `git -C #{dir} status --porcelain 2>/dev/null`.strip.empty?,
|
|
118
|
+
branch: `git -C #{dir} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip,
|
|
119
|
+
tags: `git -C #{dir} tag --points-at HEAD 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# A gem's version.rb, chosen by content rather than by name.
|
|
126
|
+
#
|
|
127
|
+
# `lib/**/version.rb` can match more than one file — this gem has both
|
|
128
|
+
# `lib/inquirex/tools/version.rb` and the CLI's
|
|
129
|
+
# `lib/inquirex/tools/commands/version.rb`, and the latter sorts first
|
|
130
|
+
# alphabetically. Requiring the VERSION constant excludes impostors;
|
|
131
|
+
# preferring the shallowest path picks the canonical one when several
|
|
132
|
+
# legitimately define it.
|
|
133
|
+
#
|
|
134
|
+
# @param name [String] package name
|
|
135
|
+
# @return [String, nil]
|
|
136
|
+
def gem_version_file(name)
|
|
137
|
+
Dir.glob(File.join(@root, name, "lib/**/version.rb"))
|
|
138
|
+
.select { |file| File.read(file).match?(GEM_VERSION_RE) }
|
|
139
|
+
.min_by { |file| [file.count("/"), file] }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @param relative [String]
|
|
143
|
+
# @return [String, nil] absolute path when it exists
|
|
144
|
+
def existing(relative)
|
|
145
|
+
path = File.join(@root, relative)
|
|
146
|
+
File.exist?(path) ? path : nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tools/version"
|
|
4
|
+
require_relative "tools/workspace"
|
|
5
|
+
require_relative "tools/versions"
|
|
6
|
+
require_relative "tools/changelogs"
|
|
7
|
+
|
|
8
|
+
module Inquirex
|
|
9
|
+
# Shared tooling for the Inquirex family.
|
|
10
|
+
#
|
|
11
|
+
# This gem is the home for anything several packages need but none of them
|
|
12
|
+
# owns — release plumbing today, and whatever else turns out to be duplicated
|
|
13
|
+
# across the gems tomorrow.
|
|
14
|
+
#
|
|
15
|
+
# It ships developer executables, so it may take dependencies the runtime
|
|
16
|
+
# gems cannot. That is the point of separating it: `inquirex` itself is
|
|
17
|
+
# required to have zero runtime dependencies, and nothing added here changes
|
|
18
|
+
# that, because nothing here is loaded by the runtime gems.
|
|
19
|
+
#
|
|
20
|
+
# Executables:
|
|
21
|
+
#
|
|
22
|
+
# - `inquirex-version` — version parity across the lockstep packages
|
|
23
|
+
# - `inquirex-changelogs` — regenerate CHANGELOG.md from merged PRs
|
|
24
|
+
#
|
|
25
|
+
# @example Check whether the family is releasable
|
|
26
|
+
# Inquirex::Tools::Versions.new.preflight # => true when every package is ready
|
|
27
|
+
module Tools
|
|
28
|
+
class Error < StandardError; end
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inquirex-tools
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Konstantin Gredeskoul
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dry-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
description: |
|
|
27
|
+
Ships the `inquirex` command: lockstep version management across the
|
|
28
|
+
Inquirex gems and npm packages, changelog regeneration, and release
|
|
29
|
+
preflight checks. A developer tool — it never depends on the gems it
|
|
30
|
+
manages, so `inquirex` itself stays dependency-free.
|
|
31
|
+
email:
|
|
32
|
+
- kigster@gmail.com
|
|
33
|
+
executables:
|
|
34
|
+
- inquirex
|
|
35
|
+
extensions: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
files:
|
|
38
|
+
- ".DS_Store"
|
|
39
|
+
- ".github_changelog_generator"
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- Rakefile
|
|
44
|
+
- exe/inquirex
|
|
45
|
+
- lib/inquirex/tools.rb
|
|
46
|
+
- lib/inquirex/tools/changelogs.rb
|
|
47
|
+
- lib/inquirex/tools/commands.rb
|
|
48
|
+
- lib/inquirex/tools/commands/changelogs.rb
|
|
49
|
+
- lib/inquirex/tools/commands/version.rb
|
|
50
|
+
- lib/inquirex/tools/commands/versions.rb
|
|
51
|
+
- lib/inquirex/tools/version.rb
|
|
52
|
+
- lib/inquirex/tools/versions.rb
|
|
53
|
+
- lib/inquirex/tools/workspace.rb
|
|
54
|
+
- sig/inquirex/tools.rbs
|
|
55
|
+
homepage: https://github.com/kigster/inquirex-tools
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
allowed_push_host: https://rubygems.org
|
|
60
|
+
homepage_uri: https://github.com/kigster/inquirex-tools
|
|
61
|
+
source_code_uri: https://github.com/kigster/inquirex-tools
|
|
62
|
+
changelog_uri: https://github.com/kigster/inquirex-tools/blob/main/CHANGELOG.md
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 4.0.17
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Release tooling for the Inquirex package family
|
|
80
|
+
test_files: []
|