inquirex-tools 0.8.0 → 0.9.4
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 +4 -4
- data/.env.encrypted +11 -0
- data/.env.example +5 -0
- data/.envrc +1 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.sops.yaml +6 -0
- data/.sopsy.sha +1 -0
- data/.sopsy.yml +11 -0
- data/exe/inquirex +3 -2
- data/exe/inquirexor +1 -0
- data/justfile +158 -0
- data/lib/inquirex/tools/commands/changelogs.rb +5 -3
- data/lib/inquirex/tools/commands/publish.rb +36 -0
- data/lib/inquirex/tools/commands/versions_bump.rb +34 -0
- data/lib/inquirex/tools/commands/versions_check.rb +36 -0
- data/lib/inquirex/tools/commands.rb +7 -3
- data/lib/inquirex/tools/publisher.rb +160 -0
- data/lib/inquirex/tools/version.rb +1 -1
- data/lib/inquirex/tools/version_bumper.rb +169 -0
- data/lib/inquirex/tools/version_checker.rb +110 -0
- data/lib/inquirex/tools/workspace.rb +30 -27
- data/lib/inquirex/tools.rb +10 -5
- metadata +31 -3
- data/lib/inquirex/tools/commands/versions.rb +0 -41
- data/lib/inquirex/tools/versions.rb +0 -125
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colored2"
|
|
4
|
+
require_relative "workspace"
|
|
5
|
+
|
|
6
|
+
module Inquirex
|
|
7
|
+
module Tools
|
|
8
|
+
# Publishes every package in the family by running each one's own
|
|
9
|
+
# `just publish` in its own directory.
|
|
10
|
+
#
|
|
11
|
+
# How a package reaches its registry stays that package's business: the
|
|
12
|
+
# gems build a .gem and `gem push` it, the npm packages run bun or yarn.
|
|
13
|
+
# This class only decides the order, supplies the one thing a package
|
|
14
|
+
# cannot work out for itself — a fresh 2FA code — and stops at the first
|
|
15
|
+
# failure.
|
|
16
|
+
#
|
|
17
|
+
# **A TOTP is single-use.** Reading 1Password once per gem inside a
|
|
18
|
+
# 30-second window returns the same six digits every time, and RubyGems
|
|
19
|
+
# rejects the second push as a replay. So a code is read per gem and the
|
|
20
|
+
# run waits for it to actually rotate before moving on, which is why
|
|
21
|
+
# publishing four gems takes minutes rather than seconds. The npm packages
|
|
22
|
+
# read their own code and are called with no argument.
|
|
23
|
+
#
|
|
24
|
+
# Order comes from {Workspace::LOCKSTEP}, so adding a package to the family
|
|
25
|
+
# adds it to the release. That order puts the core gem first, which matters:
|
|
26
|
+
# inquirex-llm and inquirex-tty depend on it and cannot resolve until it is
|
|
27
|
+
# on RubyGems.
|
|
28
|
+
#
|
|
29
|
+
# @example Preview a release without touching a registry
|
|
30
|
+
# Inquirex::Tools::Publisher.new.call(dry_run: true)
|
|
31
|
+
class Publisher
|
|
32
|
+
# How long to wait for a TOTP to rotate before giving up and using what
|
|
33
|
+
# 1Password last returned. Codes rotate every 30 seconds; 90 covers a
|
|
34
|
+
# window boundary landing badly without hanging a release indefinitely.
|
|
35
|
+
ROTATE_TIMEOUT = 90
|
|
36
|
+
|
|
37
|
+
# Seconds between polls of 1Password while waiting for a new code.
|
|
38
|
+
ROTATE_INTERVAL = 2
|
|
39
|
+
|
|
40
|
+
# Shells out with the child's stdout and stderr left attached, so a
|
|
41
|
+
# failing package's own output is what the operator sees.
|
|
42
|
+
DEFAULT_RUNNER = lambda { |dir, argv|
|
|
43
|
+
Dir.chdir(dir) { system("just", *argv) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Reads the current RubyGems TOTP. `op` resolves the account from
|
|
47
|
+
# OP_ACCOUNT, which `just publish-all` exports by decrypting
|
|
48
|
+
# .env.encrypted. Returns nil when unavailable, so a release without
|
|
49
|
+
# 1Password degrades to `gem push` prompting rather than failing.
|
|
50
|
+
DEFAULT_OTP_READER = lambda {
|
|
51
|
+
ref = "op://open-source-repos/ruby-gems/one-time password?attribute=otp"
|
|
52
|
+
code = `op read #{ref.inspect} 2>/dev/null`.strip
|
|
53
|
+
code.empty? ? nil : code
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# @param workspace [Workspace] the ecosystem checkout to publish from
|
|
57
|
+
# @param out [IO] where report output goes
|
|
58
|
+
# @param runner [#call] invoked with (dir, argv) to run a command; the
|
|
59
|
+
# default shells out. Injected so specs can assert what would run
|
|
60
|
+
# without publishing anything.
|
|
61
|
+
# @param otp_reader [#call] returns the current 2FA code, or nil
|
|
62
|
+
def initialize(workspace: Workspace.new,
|
|
63
|
+
out: $stdout,
|
|
64
|
+
runner: DEFAULT_RUNNER,
|
|
65
|
+
otp_reader: DEFAULT_OTP_READER)
|
|
66
|
+
@workspace = workspace
|
|
67
|
+
@out = out
|
|
68
|
+
@runner = runner
|
|
69
|
+
@otp_reader = otp_reader
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Publishes every lockstep package, or prints what it would publish.
|
|
73
|
+
#
|
|
74
|
+
# @param dry_run [Boolean] print the commands and run nothing
|
|
75
|
+
# @param args [Array<String>] extra arguments forwarded verbatim to each
|
|
76
|
+
# package's `just publish`
|
|
77
|
+
# @return [Boolean] true when every package published (or was printed)
|
|
78
|
+
def call(dry_run: false, args: [])
|
|
79
|
+
out.puts(dry_run ? "DRY RUN — these commands would run, in this order:" : "Publishing the family:")
|
|
80
|
+
|
|
81
|
+
last_otp = nil
|
|
82
|
+
Workspace::LOCKSTEP.each do |name, kind|
|
|
83
|
+
dir = File.join(workspace.root, name)
|
|
84
|
+
raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)
|
|
85
|
+
|
|
86
|
+
argv = ["publish", *args]
|
|
87
|
+
if dry_run
|
|
88
|
+
row(name, kind, "just #{argv.join(" ")}")
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Only the gems take a code: each npm package reads its own.
|
|
93
|
+
if kind == :gem
|
|
94
|
+
last_otp = fresh_otp(last_otp)
|
|
95
|
+
argv += [last_otp] if last_otp
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
out.puts
|
|
99
|
+
out.puts "━━ #{name} (#{kind}) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
100
|
+
return false unless run(name, dir, argv)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
out.puts
|
|
104
|
+
out.puts(dry_run ? "Nothing was published." : "Published every package.".green)
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# @return [Workspace] the ecosystem checkout being published
|
|
111
|
+
attr_reader :workspace
|
|
112
|
+
|
|
113
|
+
# @return [IO] where report output goes
|
|
114
|
+
attr_reader :out
|
|
115
|
+
|
|
116
|
+
# @return [#call] command runner
|
|
117
|
+
attr_reader :runner
|
|
118
|
+
|
|
119
|
+
# @return [#call] source of 2FA codes
|
|
120
|
+
attr_reader :otp_reader
|
|
121
|
+
|
|
122
|
+
# @return [Boolean] whether the package published cleanly
|
|
123
|
+
def run(name, dir, argv)
|
|
124
|
+
return true if runner.call(dir, argv)
|
|
125
|
+
|
|
126
|
+
out.puts
|
|
127
|
+
out.puts "#{"ERROR".bold.red}: #{name} failed `just #{argv.first}` — stopping here."
|
|
128
|
+
out.puts "Publishing is irreversible, so the packages after #{name} were left alone."
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Blocks until 1Password returns a code that has not already been spent.
|
|
133
|
+
#
|
|
134
|
+
# @param last [String, nil] the code the previous gem used
|
|
135
|
+
# @return [String, nil] a fresh code, or nil when 1Password is unavailable
|
|
136
|
+
def fresh_otp(last)
|
|
137
|
+
waited = 0
|
|
138
|
+
loop do
|
|
139
|
+
code = otp_reader.call
|
|
140
|
+
return nil if code.nil?
|
|
141
|
+
return code if code != last
|
|
142
|
+
return code if waited >= ROTATE_TIMEOUT
|
|
143
|
+
|
|
144
|
+
out.puts " waiting for the 2FA code to rotate…" if waited.zero?
|
|
145
|
+
sleep ROTATE_INTERVAL
|
|
146
|
+
waited += ROTATE_INTERVAL
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Pads before colorizing: an ANSI escape is several characters wide to
|
|
151
|
+
# `format` and zero columns wide on screen, so colorizing first left the
|
|
152
|
+
# table ragged by exactly the length of the escape codes.
|
|
153
|
+
#
|
|
154
|
+
# @return [void]
|
|
155
|
+
def row(name, kind, detail)
|
|
156
|
+
out.puts " #{name.ljust(18).yellow} #{kind.to_s.ljust(4).cyan} #{detail}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colored2"
|
|
4
|
+
require "rubygems/version"
|
|
5
|
+
require_relative "workspace"
|
|
6
|
+
|
|
7
|
+
module Inquirex
|
|
8
|
+
module Tools
|
|
9
|
+
# Moves every lockstep package to one version, then proves each one still
|
|
10
|
+
# builds — the write half of the pair whose read half is {VersionChecker}.
|
|
11
|
+
#
|
|
12
|
+
# The family shares a serialization format, so a version that identifies
|
|
13
|
+
# one package identifies all of them. Bumping is therefore all-or-nothing:
|
|
14
|
+
# there is no supported way to move a single package.
|
|
15
|
+
#
|
|
16
|
+
# Writing a version and verifying it are one command on purpose. A version
|
|
17
|
+
# written but never checked is the failure this is meant to prevent: the
|
|
18
|
+
# number says the family agrees while one package no longer passes its own
|
|
19
|
+
# suite.
|
|
20
|
+
#
|
|
21
|
+
# @example Move the family to 0.9.2 and check every package
|
|
22
|
+
# Inquirex::Tools::VersionBumper.new.call("0.9.2") # => false if any check fails
|
|
23
|
+
class VersionBumper
|
|
24
|
+
# A version must be exactly MAJOR.MINOR.PATCH — no prereleases, because
|
|
25
|
+
# the npm packages and the gems would spell them differently.
|
|
26
|
+
SEMVER_RE = /\A\d+\.\d+\.\d+\z/
|
|
27
|
+
|
|
28
|
+
# Prefix for the refusal messages. This command's only feedback is what
|
|
29
|
+
# it prints, so the word carries weight.
|
|
30
|
+
ERROR = "ERROR"
|
|
31
|
+
|
|
32
|
+
# What each package is verified with. One recipe for gems and npm
|
|
33
|
+
# packages alike — every repo's justfile defines it, which is the point
|
|
34
|
+
# of having a justfile in each.
|
|
35
|
+
CHECK = %w[just check-all].freeze
|
|
36
|
+
|
|
37
|
+
# Rule across the report, sized to the rows.
|
|
38
|
+
RULE = ("—" * 52).freeze
|
|
39
|
+
|
|
40
|
+
# @param workspace [Workspace] the ecosystem checkout to rewrite
|
|
41
|
+
# @param out [IO] where report output goes
|
|
42
|
+
def initialize(workspace: Workspace.new, out: $stdout)
|
|
43
|
+
@workspace = workspace
|
|
44
|
+
@out = out
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Rewrites every lockstep package to the target version, then runs each
|
|
48
|
+
# package's own `just check-all`.
|
|
49
|
+
#
|
|
50
|
+
# Refuses to move backwards: a version already published cannot be
|
|
51
|
+
# reissued, so the target must be at least the highest current value.
|
|
52
|
+
#
|
|
53
|
+
# @param version [String] target, MAJOR.MINOR.PATCH
|
|
54
|
+
# @param verify [Boolean] run `just check-all` per package afterwards
|
|
55
|
+
# @return [Boolean] true when every package was written and checked
|
|
56
|
+
def call(version, verify: true)
|
|
57
|
+
return false unless valid?(version)
|
|
58
|
+
|
|
59
|
+
out.puts "Setting all lockstep packages to #{version} 🚨:"
|
|
60
|
+
each_package { |name, kind| write(name, kind, version) }
|
|
61
|
+
return true unless verify
|
|
62
|
+
|
|
63
|
+
out.puts
|
|
64
|
+
out.puts "Verifying each package with `#{CHECK.join(" ")}`:"
|
|
65
|
+
results = each_package { |name, _kind| check(name) }
|
|
66
|
+
report(results)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# @return [Workspace] the ecosystem checkout being rewritten
|
|
72
|
+
attr_reader :workspace
|
|
73
|
+
|
|
74
|
+
# @return [IO] where report output goes
|
|
75
|
+
attr_reader :out
|
|
76
|
+
|
|
77
|
+
# The one place the package list is walked, so adding a package changes
|
|
78
|
+
# nothing here.
|
|
79
|
+
#
|
|
80
|
+
# @yieldparam name [String] package name
|
|
81
|
+
# @yieldparam kind [Symbol] :gem or :npm
|
|
82
|
+
# @return [Hash{String => Object}] name => whatever the block returned
|
|
83
|
+
def each_package
|
|
84
|
+
Workspace::LOCKSTEP.to_h { |name, kind| [name, yield(name, kind)] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [void]
|
|
88
|
+
def write(name, kind, version)
|
|
89
|
+
workspace.write_version(name, version, kind)
|
|
90
|
+
row(name, kind.to_s.cyan, version.green)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Runs one package's checks in its own directory.
|
|
94
|
+
#
|
|
95
|
+
# A package with no justfile is reported and skipped rather than failed:
|
|
96
|
+
# the fixture used by the specs has none, and a repo that has not grown
|
|
97
|
+
# one yet should not make a legitimate bump look broken.
|
|
98
|
+
#
|
|
99
|
+
# @param name [String] package name
|
|
100
|
+
# @return [Boolean, nil] pass/fail, or nil when skipped
|
|
101
|
+
def check(name)
|
|
102
|
+
dir = File.join(workspace.root, name)
|
|
103
|
+
passed = Dir.chdir(dir) do
|
|
104
|
+
row(name, "running #{CHECK.join(" ")} in ".yellow, dir)
|
|
105
|
+
out.puts RULE
|
|
106
|
+
system("git fetch")
|
|
107
|
+
system("test -n $(git status --porcelain) && { git stash -u; echo 'Stashing locally modified files...'; }")
|
|
108
|
+
system("git pull origin main")
|
|
109
|
+
system("git fetch --tags")
|
|
110
|
+
system("git rebase origin/main")
|
|
111
|
+
unless File.exist?(File.join(dir, "justfile"))
|
|
112
|
+
row(name, "skip".yellow, "no justfile".yellow)
|
|
113
|
+
return nil
|
|
114
|
+
end
|
|
115
|
+
row(name, "Pulled in #{name}, current version is #{system("just version")}", dir)
|
|
116
|
+
system(*CHECK)
|
|
117
|
+
end
|
|
118
|
+
row(name, passed ? "[ OK ]".on.green : "[ 🅧 ]".on_red, dir)
|
|
119
|
+
passed
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The single formatting site for every line of the report.
|
|
123
|
+
#
|
|
124
|
+
# `format`, never `printf`: printf writes straight to $stdout and returns
|
|
125
|
+
# nil, so an injected out would receive one blank line per package while
|
|
126
|
+
# the real output went elsewhere.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def row(name, state, detail)
|
|
130
|
+
out.puts format(" → %<name>-24s %<state>-6s %<detail>s", name: name.yellow, state:, detail:)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param results [Hash{String => Boolean, nil}] per-package check outcome
|
|
134
|
+
# @return [Boolean] true when nothing failed
|
|
135
|
+
def report(results)
|
|
136
|
+
failed = results.select { |_name, passed| passed == false }.keys
|
|
137
|
+
out.puts RULE
|
|
138
|
+
if failed.empty?
|
|
139
|
+
out.puts "OK: every package checked out".green
|
|
140
|
+
true
|
|
141
|
+
else
|
|
142
|
+
out.puts "#{ERROR.bold.red}: #{failed.join(", ")} failed `#{CHECK.join(" ")}`"
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Whether the target is well-formed and moves the family forward.
|
|
148
|
+
# Reports the reason it does not, since this is the CLI's only feedback.
|
|
149
|
+
#
|
|
150
|
+
# @param version [String] proposed target
|
|
151
|
+
# @return [Boolean]
|
|
152
|
+
def valid?(version)
|
|
153
|
+
unless SEMVER_RE.match?(version.to_s)
|
|
154
|
+
out.puts "#{ERROR.bold.red}: #{version.inspect} is not MAJOR.MINOR.PATCH"
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
highest = workspace.versions.values.compact.max_by { Gem::Version.new(it) }
|
|
159
|
+
if highest && Gem::Version.new(highest) > Gem::Version.new(version)
|
|
160
|
+
out.puts "#{ERROR.bold.red}: #{version} is below the highest existing " \
|
|
161
|
+
"version (#{highest}) — versions only go forward"
|
|
162
|
+
return false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colored2"
|
|
4
|
+
require_relative "workspace"
|
|
5
|
+
|
|
6
|
+
module Inquirex
|
|
7
|
+
module Tools
|
|
8
|
+
# Reports version parity across the lockstep packages.
|
|
9
|
+
#
|
|
10
|
+
# Read-only by construction: nothing here writes a version file. Bumping is
|
|
11
|
+
# {VersionBumper}'s job. The split means a check can be run anywhere — CI, a
|
|
12
|
+
# dirty tree, a colleague's checkout — with no chance of editing something,
|
|
13
|
+
# and the two behaviours no longer share a flag that decides which happens.
|
|
14
|
+
#
|
|
15
|
+
# @example Check for drift
|
|
16
|
+
# Inquirex::Tools::VersionChecker.new.call # => false when versions differ
|
|
17
|
+
class VersionChecker
|
|
18
|
+
# @param workspace [Workspace] the ecosystem checkout to inspect
|
|
19
|
+
# @param out [IO] where report output goes
|
|
20
|
+
def initialize(workspace: Workspace.new, out: $stdout)
|
|
21
|
+
@workspace = workspace
|
|
22
|
+
@out = out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Prints the current state of every lockstep package.
|
|
26
|
+
#
|
|
27
|
+
# @return [Boolean] true when all packages agree on one version
|
|
28
|
+
def call
|
|
29
|
+
versions = workspace.versions
|
|
30
|
+
print_table(versions)
|
|
31
|
+
verdict(versions)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Parity plus git readiness: clean tree, on main, tagged at the version.
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean] true when every package could be published now
|
|
37
|
+
def preflight
|
|
38
|
+
ok = call
|
|
39
|
+
target = workspace.versions.values.compact.uniq.first
|
|
40
|
+
out.puts
|
|
41
|
+
out.puts "Release readiness:"
|
|
42
|
+
|
|
43
|
+
Workspace::LOCKSTEP.each_key do |name|
|
|
44
|
+
problems = readiness_problems(name, target)
|
|
45
|
+
out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready".bright_green : problems.join(", ").red)
|
|
46
|
+
ok &&= problems.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
out.puts
|
|
50
|
+
out.puts ok ? "ALL READY — publish with `gem push` / `npm publish`" : "NOT READY — resolve the above"
|
|
51
|
+
ok
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# @return [Workspace] the ecosystem checkout being inspected
|
|
57
|
+
attr_reader :workspace
|
|
58
|
+
|
|
59
|
+
# @return [IO] where report output goes
|
|
60
|
+
attr_reader :out
|
|
61
|
+
|
|
62
|
+
# @param versions [Hash{String => String, nil}]
|
|
63
|
+
# @return [void]
|
|
64
|
+
def print_table(versions)
|
|
65
|
+
versions.each do |name, version|
|
|
66
|
+
# format, not printf: printf writes straight to $stdout and returns
|
|
67
|
+
# nil, so the injected out saw one blank line per package and the
|
|
68
|
+
# real table bypassed it entirely.
|
|
69
|
+
out.puts format(" → %<name>-30s %<kind>5s %<version>6s",
|
|
70
|
+
name: name.to_s.yellow,
|
|
71
|
+
kind: Workspace::LOCKSTEP[name].to_s.cyan,
|
|
72
|
+
version: (version || "MISSING").to_s.green)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param versions [Hash{String => String, nil}]
|
|
77
|
+
# @return [Boolean] true when every package agrees on one version
|
|
78
|
+
def verdict(versions)
|
|
79
|
+
out.puts "——————————————————————————————————"
|
|
80
|
+
distinct = versions.values.compact.uniq
|
|
81
|
+
if versions.value?(nil)
|
|
82
|
+
out.puts "DRIFT: a version could not be read (MISSING above)".red
|
|
83
|
+
false
|
|
84
|
+
elsif distinct.one?
|
|
85
|
+
out.puts "OK: all #{versions.size} at #{distinct.first}".green
|
|
86
|
+
true
|
|
87
|
+
else
|
|
88
|
+
out.puts "DRIFT: #{distinct.sort.join(", ")} — run `inquirex versions-bump <version>`".yellow
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Why a package is not publishable right now.
|
|
94
|
+
#
|
|
95
|
+
# @param name [String] package name
|
|
96
|
+
# @param target [String, nil] the version its tag must match
|
|
97
|
+
# @return [Array<String>] empty when ready
|
|
98
|
+
def readiness_problems(name, target)
|
|
99
|
+
state = workspace.git_state(name)
|
|
100
|
+
return ["no git checkout"] if state[:clean].nil?
|
|
101
|
+
|
|
102
|
+
problems = []
|
|
103
|
+
problems << "DIRTY" unless state[:clean]
|
|
104
|
+
problems << "on #{state[:branch]}" unless state[:branch] == "main"
|
|
105
|
+
problems << "untagged" unless state[:tags].any? { |tag| tag.delete_prefix("v") == target }
|
|
106
|
+
problems
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -11,34 +11,34 @@ module Inquirex
|
|
|
11
11
|
# walking up from the working directory looking for the sibling checkouts,
|
|
12
12
|
# or taken from INQUIREX_ROOT.
|
|
13
13
|
class Workspace
|
|
14
|
-
#
|
|
15
|
-
# serialization format — the gem defines the DSL and emits step JSON,
|
|
16
|
-
# inquirex-
|
|
17
|
-
# inquirex-
|
|
18
|
-
#
|
|
19
|
-
#
|
|
14
|
+
# Every package in the family, all carrying the same version. They share
|
|
15
|
+
# one serialization format — the gem defines the DSL and emits step JSON,
|
|
16
|
+
# inquirex-widget (npm; formerly @kigster/inquirex-js) renders it,
|
|
17
|
+
# inquirex-webui prints it back to DSL, inquirex-llm extends the
|
|
18
|
+
# vocabulary, inquirex-tty walks a flow in the terminal, and
|
|
19
|
+
# inquirex-tools is the tooling that ships alongside them. A verb present
|
|
20
|
+
# in one and absent in another does not fail loudly; it drops data.
|
|
20
21
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# Nothing is excluded. One version number across the whole family means
|
|
23
|
+
# "which combination has this bug?" is a lookup rather than an
|
|
24
|
+
# investigation.
|
|
23
25
|
LOCKSTEP = {
|
|
24
|
-
"inquirex"
|
|
25
|
-
"inquirex-llm"
|
|
26
|
-
"inquirex-tools"
|
|
27
|
-
"inquirex-
|
|
28
|
-
"inquirex-
|
|
26
|
+
"inquirex" => :gem,
|
|
27
|
+
"inquirex-llm" => :gem,
|
|
28
|
+
"inquirex-tools" => :gem,
|
|
29
|
+
"inquirex-tty" => :gem,
|
|
30
|
+
"inquirex-widget" => :npm,
|
|
31
|
+
"inquirex-webui" => :npm
|
|
29
32
|
}.freeze
|
|
30
33
|
|
|
31
|
-
# Excluded from lockstep, documented so the omission reads as deliberate.
|
|
32
|
-
EXCLUDED = %w[inquirex-tty].freeze
|
|
33
|
-
|
|
34
34
|
# Repositories that carry a generated CHANGELOG.md but are not versioned
|
|
35
35
|
# in lockstep.
|
|
36
|
-
CHANGELOG_ONLY = %w[
|
|
36
|
+
CHANGELOG_ONLY = %w[qualified-at].freeze
|
|
37
37
|
|
|
38
38
|
GEM_VERSION_RE = /VERSION\s*=\s*"([^"]+)"/
|
|
39
39
|
|
|
40
40
|
# Directories whose presence identifies the ecosystem root.
|
|
41
|
-
MARKERS = %w[inquirex inquirex-
|
|
41
|
+
MARKERS = %w[inquirex inquirex-widget].freeze
|
|
42
42
|
|
|
43
43
|
# @return [String] absolute path to the ecosystem root
|
|
44
44
|
attr_reader :root
|
|
@@ -92,18 +92,21 @@ module Inquirex
|
|
|
92
92
|
#
|
|
93
93
|
# @param name [String] package name
|
|
94
94
|
# @param version [String] new version
|
|
95
|
+
# @param kind [Symbol] :gem or :npm — which version line to rewrite.
|
|
96
|
+
# Passed by the caller rather than looked up here so a caller that
|
|
97
|
+
# already knows the kind does not pay for a second lookup.
|
|
95
98
|
# @return [void]
|
|
96
|
-
def write_version(name, version)
|
|
99
|
+
def write_version(name, version, kind)
|
|
97
100
|
file = version_file(name)
|
|
98
101
|
raise Error, "no version file for #{name}" unless file
|
|
99
102
|
|
|
100
103
|
src = File.read(file)
|
|
101
|
-
updated =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
updated = case kind
|
|
105
|
+
when :gem
|
|
106
|
+
src.sub(GEM_VERSION_RE, %(VERSION = "#{version}"))
|
|
107
|
+
when :npm
|
|
108
|
+
src.sub(/"version":\s*"[^"]*"/, %("version": "#{version}"))
|
|
109
|
+
end
|
|
107
110
|
File.write(file, updated)
|
|
108
111
|
end
|
|
109
112
|
|
|
@@ -114,9 +117,9 @@ module Inquirex
|
|
|
114
117
|
return { clean: nil, branch: nil, tags: [] } unless File.exist?(File.join(dir, ".git"))
|
|
115
118
|
|
|
116
119
|
{
|
|
117
|
-
clean:
|
|
120
|
+
clean: `git -C #{dir} status --porcelain 2>/dev/null`.strip.empty?,
|
|
118
121
|
branch: `git -C #{dir} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip,
|
|
119
|
-
tags:
|
|
122
|
+
tags: `git -C #{dir} tag --points-at HEAD 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?)
|
|
120
123
|
}
|
|
121
124
|
end
|
|
122
125
|
|
data/lib/inquirex/tools.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "tools/version"
|
|
4
4
|
require_relative "tools/workspace"
|
|
5
|
-
require_relative "tools/
|
|
5
|
+
require_relative "tools/version_checker"
|
|
6
|
+
require_relative "tools/version_bumper"
|
|
7
|
+
require_relative "tools/publisher"
|
|
6
8
|
require_relative "tools/changelogs"
|
|
7
9
|
|
|
8
10
|
module Inquirex
|
|
@@ -17,13 +19,16 @@ module Inquirex
|
|
|
17
19
|
# required to have zero runtime dependencies, and nothing added here changes
|
|
18
20
|
# that, because nothing here is loaded by the runtime gems.
|
|
19
21
|
#
|
|
20
|
-
#
|
|
22
|
+
# It ships one executable, `inquirex`, with these subcommands:
|
|
21
23
|
#
|
|
22
|
-
# - `
|
|
23
|
-
# - `
|
|
24
|
+
# - `versions-check` — version parity across the lockstep packages
|
|
25
|
+
# - `versions-bump` — move every lockstep package to one version
|
|
26
|
+
# - `publish` — publish every package to RubyGems and npm
|
|
27
|
+
# - `changelogs` — regenerate CHANGELOG.md from merged PRs
|
|
28
|
+
# - `version` — this gem's own version
|
|
24
29
|
#
|
|
25
30
|
# @example Check whether the family is releasable
|
|
26
|
-
# Inquirex::Tools::
|
|
31
|
+
# Inquirex::Tools::VersionChecker.new.preflight # => true when every package is ready
|
|
27
32
|
module Tools
|
|
28
33
|
class Error < StandardError; end
|
|
29
34
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inquirex-tools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: colored2
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
description: |
|
|
27
41
|
Ships the `inquirex` command: lockstep version management across the
|
|
28
42
|
Inquirex gems and npm packages, changelog regeneration, and release
|
|
@@ -32,24 +46,38 @@ email:
|
|
|
32
46
|
- kigster@gmail.com
|
|
33
47
|
executables:
|
|
34
48
|
- inquirex
|
|
49
|
+
- inquirexor
|
|
35
50
|
extensions: []
|
|
36
51
|
extra_rdoc_files: []
|
|
37
52
|
files:
|
|
38
53
|
- ".DS_Store"
|
|
54
|
+
- ".env.encrypted"
|
|
55
|
+
- ".env.example"
|
|
56
|
+
- ".envrc"
|
|
39
57
|
- ".github_changelog_generator"
|
|
58
|
+
- ".relaxed_rubocop.yml"
|
|
59
|
+
- ".sops.yaml"
|
|
60
|
+
- ".sopsy.sha"
|
|
61
|
+
- ".sopsy.yml"
|
|
40
62
|
- CHANGELOG.md
|
|
41
63
|
- LICENSE.txt
|
|
42
64
|
- README.md
|
|
43
65
|
- Rakefile
|
|
44
66
|
- exe/inquirex
|
|
67
|
+
- exe/inquirexor
|
|
68
|
+
- justfile
|
|
45
69
|
- lib/inquirex/tools.rb
|
|
46
70
|
- lib/inquirex/tools/changelogs.rb
|
|
47
71
|
- lib/inquirex/tools/commands.rb
|
|
48
72
|
- lib/inquirex/tools/commands/changelogs.rb
|
|
73
|
+
- lib/inquirex/tools/commands/publish.rb
|
|
49
74
|
- lib/inquirex/tools/commands/version.rb
|
|
50
|
-
- lib/inquirex/tools/commands/
|
|
75
|
+
- lib/inquirex/tools/commands/versions_bump.rb
|
|
76
|
+
- lib/inquirex/tools/commands/versions_check.rb
|
|
77
|
+
- lib/inquirex/tools/publisher.rb
|
|
51
78
|
- lib/inquirex/tools/version.rb
|
|
52
|
-
- lib/inquirex/tools/
|
|
79
|
+
- lib/inquirex/tools/version_bumper.rb
|
|
80
|
+
- lib/inquirex/tools/version_checker.rb
|
|
53
81
|
- lib/inquirex/tools/workspace.rb
|
|
54
82
|
- sig/inquirex/tools.rbs
|
|
55
83
|
homepage: https://github.com/kigster/inquirex-tools
|