inquirex-tools 0.9.4 → 0.9.5
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/docs/badges/coverage_badge.svg +21 -0
- data/docs/monorepo-migration.md +163 -0
- data/lib/inquirex/tools/changelogs.rb +21 -3
- data/lib/inquirex/tools/commands/create_release.rb +56 -0
- data/lib/inquirex/tools/commands/publish.rb +17 -3
- data/lib/inquirex/tools/commands.rb +4 -0
- data/lib/inquirex/tools/publisher.rb +74 -5
- data/lib/inquirex/tools/releaser.rb +163 -0
- data/lib/inquirex/tools/version.rb +1 -1
- data/lib/inquirex/tools/version_bumper.rb +77 -16
- data/lib/inquirex/tools/version_checker.rb +4 -1
- data/lib/inquirex/tools.rb +2 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8f0e3e2dc5bcc9d45d3c9cba64f8a7d96400b6d1296d316ff56460ece229166
|
|
4
|
+
data.tar.gz: 3ee07acd2e8e731115e0a5552bbbdaeea2979198195e396fc946765f8b6deef6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81ca8eed0f60037439fd3c8ab6aafe9b35114082f2c02db488bf279a3d52577c7fb7c8c7e04a41ab48e0c8ab6be1e8e71e324d32b566cbc865ce861e4855601e
|
|
7
|
+
data.tar.gz: 3adc9867dab085361badf7b00ff38ef9965345efa2ef610bddb27e35e2a34945ebaa2459bdf068e18118cd2121ec67f952564f2f9df562e00d53df49ebb63578
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
|
|
3
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
4
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
5
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<mask id="a">
|
|
8
|
+
<rect width="99" height="20" rx="3" fill="#fff"/>
|
|
9
|
+
</mask>
|
|
10
|
+
<g mask="url(#a)">
|
|
11
|
+
<path fill="#555" d="M0 0h63v20H0z"/>
|
|
12
|
+
<path fill="#4c1" d="M63 0h36v20H63z"/>
|
|
13
|
+
<path fill="url(#b)" d="M0 0h99v20H0z"/>
|
|
14
|
+
</g>
|
|
15
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
16
|
+
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
17
|
+
<text x="31.5" y="14">coverage</text>
|
|
18
|
+
<text x="80" y="15" fill="#010101" fill-opacity=".3">97%</text>
|
|
19
|
+
<text x="80" y="14">97%</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Monorepo Migration Plan
|
|
2
|
+
|
|
3
|
+
Status: **proposed**, not started. Execute only after the open PRs listed under [Sequencing](#sequencing) have merged.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The Inquirex ecosystem implements the same semantics in two languages and enforces agreement by convention. Three independent implementations of "canonicalize an LLM answer against a question's allowed values" exist today:
|
|
8
|
+
|
|
9
|
+
| Implementation | Repo | Exact | Case-insensitive value | Label fallback | Strips whitespace |
|
|
10
|
+
| ------------------------- | --------------- | ----- | ---------------------- | -------------- | ----------------- |
|
|
11
|
+
| `Node#resolve_option` | inquirex | yes | yes | yes | no |
|
|
12
|
+
| `Adapter#canonical_value` | inquirex-llm | yes | yes | **no** | **yes** |
|
|
13
|
+
| `canonicalOption` | inquirex-widget | yes | yes | yes | **no** |
|
|
14
|
+
|
|
15
|
+
They disagree on real inputs. `"US citizen or permanent resident"` resolves through the gem and the widget but returns `nil` through the LLM adapter. `" us_person "` resolves only through the LLM adapter. Nothing in CI can observe this, because no CI job sees more than one of them.
|
|
16
|
+
|
|
17
|
+
This is the failure mode the family already has scar tissue for. The gem shipped `required false` in 0.7.0 and no consumer knew, because a verb present in one package and missing in another does not fail loudly — it silently drops data. The lockstep version rule exists to make "which combination has this bug?" a lookup rather than an investigation.
|
|
18
|
+
|
|
19
|
+
The rule is not actually enforced. Every inter-family gemspec declares `~> 0.6`, which admits any 0.x from 0.6 upward. `inquirex-llm`'s checked-in `Gemfile.lock` currently resolves `inquirex-llm (0.9.2)` against `inquirex (0.8.0)`.
|
|
20
|
+
|
|
21
|
+
A monorepo does not fix divergence by itself. It makes the fix — one shared fixture set, executed by every implementation in one CI run — cheap enough to actually build.
|
|
22
|
+
|
|
23
|
+
## Scope
|
|
24
|
+
|
|
25
|
+
**In**, as one public repository:
|
|
26
|
+
|
|
27
|
+
- `inquirex` — the DSL, engine, and rule AST
|
|
28
|
+
- `inquirex-llm` — LLM verbs
|
|
29
|
+
- `inquirex-widget` — the embeddable TypeScript widget
|
|
30
|
+
- `inquirex-tty` — terminal adapter
|
|
31
|
+
- `inquirex-tools` — release and version tooling (largely absorbed; see below)
|
|
32
|
+
|
|
33
|
+
**Out**, staying as they are:
|
|
34
|
+
|
|
35
|
+
- `qualified-at` — the commercial Rails application. Private, its own release cadence, and it consumes the libraries as published packages. Folding a revenue product into the open-source repository forces a visibility decision on the whole tree and buys nothing.
|
|
36
|
+
- `inquirex-webui` — private, React, a distinct product (visual builder). Revisit once the library tree has settled.
|
|
37
|
+
- `inquirex-presentation`, `inquirex-slides`, `claude-setup` — unrelated artifacts.
|
|
38
|
+
|
|
39
|
+
The in-scope set is exactly the packages bound by the lockstep rule, minus `inquirex-webui`, plus `inquirex-tty` (which PR #2 in this repo brings into lockstep).
|
|
40
|
+
|
|
41
|
+
## Target layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
inquirex/
|
|
45
|
+
packages/
|
|
46
|
+
inquirex/ # gem: DSL, engine, rules
|
|
47
|
+
inquirex-llm/ # gem: LLM verbs
|
|
48
|
+
inquirex-tty/ # gem: terminal adapter
|
|
49
|
+
inquirex-widget/ # npm: TypeScript widget
|
|
50
|
+
conformance/
|
|
51
|
+
fixtures/ # language-neutral JSON cases
|
|
52
|
+
ruby/ # runner: executes fixtures against the gems
|
|
53
|
+
typescript/ # runner: executes fixtures against the widget
|
|
54
|
+
tools/ # release tooling (from inquirex-tools)
|
|
55
|
+
justfile # top-level recipes; per-package justfiles remain
|
|
56
|
+
.github/workflows/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each package keeps its own gemspec / package.json, test suite, and justfile. The monorepo adds a layer above them, it does not merge their build systems.
|
|
60
|
+
|
|
61
|
+
## Migration mechanics
|
|
62
|
+
|
|
63
|
+
History is preserved. Do not squash the repositories into a single import commit.
|
|
64
|
+
|
|
65
|
+
For each repository, in the order listed under Sequencing:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 1. Clone a scratch copy — never run filter-repo against your working checkout
|
|
69
|
+
git clone git@github.com:inquirex/<pkg>.git /tmp/mono/<pkg>
|
|
70
|
+
cd /tmp/mono/<pkg>
|
|
71
|
+
|
|
72
|
+
# 2. Rewrite every path to sit under packages/<pkg>/
|
|
73
|
+
git filter-repo --to-subdirectory-filter packages/<pkg>
|
|
74
|
+
|
|
75
|
+
# 3. Graft it into the monorepo as an unrelated history
|
|
76
|
+
cd /path/to/inquirex-monorepo
|
|
77
|
+
git remote add <pkg> /tmp/mono/<pkg>
|
|
78
|
+
git fetch <pkg>
|
|
79
|
+
git merge --allow-unrelated-histories <pkg>/main -m "Import <pkg> under packages/"
|
|
80
|
+
git remote remove <pkg>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`git filter-repo` requires a fresh clone and refuses to run on a repo with an origin unless forced — that is a guardrail, not an obstacle. Install with `brew install git-filter-repo`.
|
|
84
|
+
|
|
85
|
+
Path rewriting means `git log --follow packages/inquirex/lib/inquirex/node.rb` walks the full pre-migration history. Blame survives. Existing tags survive but become ambiguous across packages — retag as `<pkg>/vX.Y.Z` after the import, or accept that pre-migration tags refer to a single package's history.
|
|
86
|
+
|
|
87
|
+
### What happens to the old repositories
|
|
88
|
+
|
|
89
|
+
Archive, do not delete. Each keeps its issues, its pull requests, and its clone URLs. GitHub archived repos stay readable and redirect nothing, so add a line to each README pointing at the monorepo path. RubyGems and npm metadata should be updated at the next release to point `source_code_uri` / `repository.url` at `inquirex/inquirex` with the appropriate `directory`.
|
|
90
|
+
|
|
91
|
+
Both npm and RubyGems support a subdirectory hint:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
// packages/inquirex-widget/package.json
|
|
95
|
+
"repository": { "type": "git", "url": "git+https://github.com/inquirex/inquirex.git", "directory": "packages/inquirex-widget" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# packages/inquirex/inquirex.gemspec
|
|
100
|
+
spec.metadata["source_code_uri"] = "https://github.com/inquirex/inquirex/tree/main/packages/inquirex"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Versioning and release
|
|
104
|
+
|
|
105
|
+
One version number for the tree, which is what the lockstep rule always wanted. A single `VERSION` file at the root; each gemspec and `package.json` reads from it at build time.
|
|
106
|
+
|
|
107
|
+
This deletes most of `inquirex-tools`: cross-repo version checking and bumping becomes `just version <increment>` operating on one file. Keep the release recipes (tagging, GitHub release, publishing); drop the multi-repo coordination.
|
|
108
|
+
|
|
109
|
+
Fix the constraint hole at the same time. Inter-package dependencies move from `~> 0.6` to `= <VERSION>` or `~> <MAJOR>.<MINOR>`, so bundler cannot resolve a mismatched pair. Do this **in the release that first ships from the monorepo**, not before — tightening `inquirex-llm` to `~> 0.9` while `inquirex` 0.9.x is unpublished breaks `bundle install`.
|
|
110
|
+
|
|
111
|
+
## CI
|
|
112
|
+
|
|
113
|
+
One workflow with path filters, so a widget-only change does not run the Ruby matrix:
|
|
114
|
+
|
|
115
|
+
- `ruby.yml` — triggers on `packages/inquirex*/**` and `conformance/**`; runs each gem's suite
|
|
116
|
+
- `typescript.yml` — triggers on `packages/inquirex-widget/**` and `conformance/**`; runs `bun test`, typecheck, lint, build
|
|
117
|
+
- `conformance.yml` — triggers on **everything**; runs the shared fixtures against both language runners
|
|
118
|
+
|
|
119
|
+
The third workflow is the point of the exercise. It must not be path-filtered.
|
|
120
|
+
|
|
121
|
+
## The conformance suite
|
|
122
|
+
|
|
123
|
+
A fixture is a JSON document describing a flow, a sequence of inputs, and the expected resulting state:
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
{
|
|
127
|
+
"name": "extraction resolves a label to its form value",
|
|
128
|
+
"flow": { "...": "flow definition" },
|
|
129
|
+
"steps": [
|
|
130
|
+
{ "extract": { "residency_status": "US citizen or permanent resident" } }
|
|
131
|
+
],
|
|
132
|
+
"expect": {
|
|
133
|
+
"answers": { "residency_status": "us_person" },
|
|
134
|
+
"current_step": "income_types",
|
|
135
|
+
"suggestions": {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each language ships a thin runner that loads a fixture, drives its own engine, and asserts the expected state. Adding a case means adding one JSON file; both languages are then obliged to agree or fail.
|
|
141
|
+
|
|
142
|
+
Seed it with the divergence table above — those are three cases that fail today. Then extend to transition resolution, `skip_if`, accumulators, and multi-select suggestion handling.
|
|
143
|
+
|
|
144
|
+
This is the deliverable that justifies the migration. Everything else is filing.
|
|
145
|
+
|
|
146
|
+
## Sequencing
|
|
147
|
+
|
|
148
|
+
1. Merge the open work first. Migrating on top of open pull requests strands them: `inquirex-widget` #6-#9, `inquirex` #5/#6, `inquirex-llm` #3, `inquirex-tools` #1/#2, `inquirex-tty` #10. Close or rebase the stale ones (`inquirex-tty` #6 is conflicting and failing; `inquirex` #5 and `inquirex-tools` #1 appear superseded).
|
|
149
|
+
1. Publish 0.9.2 from the existing repositories, so a known-good release exists on both registries before anything moves.
|
|
150
|
+
1. Create the monorepo and import packages in dependency order: `inquirex`, `inquirex-llm`, `inquirex-tty`, `inquirex-widget`, `inquirex-tools`.
|
|
151
|
+
1. Wire up the top-level justfile and per-language CI. Prove every existing suite still passes unchanged.
|
|
152
|
+
1. Build the conformance harness with the three known-divergent cases. Watch them fail.
|
|
153
|
+
1. Reconcile the three implementations. Decide the canonical rules — label fallback yes or no, whitespace stripping yes or no — and make all three obey.
|
|
154
|
+
1. Move to a single root `VERSION`, tighten the inter-package constraints, release 0.10.0 from the monorepo.
|
|
155
|
+
1. Archive the old repositories with a pointer in each README.
|
|
156
|
+
|
|
157
|
+
Steps 1-2 are prerequisites. Steps 3-4 are reversible: if the imported tree is unsatisfactory, delete it — the original repositories are untouched until step 8.
|
|
158
|
+
|
|
159
|
+
## Open decisions
|
|
160
|
+
|
|
161
|
+
- **`inquirex-webui`** — in or out. It is bound by the lockstep rule but is private and a different product. Leaving it out means the rule stays partly manual.
|
|
162
|
+
- **Visibility** — the monorepo must be public for the gems to remain credible open source. That is only tenable if nothing private is imported, which the scope above respects.
|
|
163
|
+
- **Canonical matching rules** — the reconciliation in step 6 is a semantics decision, not a mechanical one. Whitespace stripping is almost certainly correct everywhere. The label fallback is the real question: it is most needed in `inquirex-llm`, which is the one implementation lacking it.
|
|
@@ -18,11 +18,29 @@ module Inquirex
|
|
|
18
18
|
# Never overwritten by generation, in any repo.
|
|
19
19
|
PROTECTED = %w[INQUIREX_VERSION_LOG.md].freeze
|
|
20
20
|
|
|
21
|
+
# Reads a GitHub token from the `gh` session rather than a stored secret,
|
|
22
|
+
# so nothing here handles a credential in plain text.
|
|
23
|
+
DEFAULT_TOKEN_READER = -> { `gh auth token 2>/dev/null`.strip }
|
|
24
|
+
|
|
25
|
+
# Runs the generator in one repository, discarding its very chatty output.
|
|
26
|
+
DEFAULT_RUNNER = lambda { |dir, env|
|
|
27
|
+
system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
# @param workspace [Workspace]
|
|
22
31
|
# @param out [IO]
|
|
23
|
-
|
|
32
|
+
# @param token_reader [#call] returns a GitHub token, or "" when there is
|
|
33
|
+
# no session. Injected so a spec never needs one.
|
|
34
|
+
# @param runner [#call] invoked with (dir, env) to run the generator.
|
|
35
|
+
# Injected so a spec never shells out to it.
|
|
36
|
+
def initialize(workspace: Workspace.new,
|
|
37
|
+
out: $stdout,
|
|
38
|
+
token_reader: DEFAULT_TOKEN_READER,
|
|
39
|
+
runner: DEFAULT_RUNNER)
|
|
24
40
|
@workspace = workspace
|
|
25
41
|
@out = out
|
|
42
|
+
@token_reader = token_reader
|
|
43
|
+
@runner = runner
|
|
26
44
|
end
|
|
27
45
|
|
|
28
46
|
# @return [Array<String>] every repo carrying a generator config
|
|
@@ -41,7 +59,7 @@ module Inquirex
|
|
|
41
59
|
# @param names [Array<String>] repositories, or all configured when empty
|
|
42
60
|
# @return [Boolean] true when every repository succeeded
|
|
43
61
|
def generate(*names)
|
|
44
|
-
token =
|
|
62
|
+
token = @token_reader.call.to_s.strip
|
|
45
63
|
if token.empty?
|
|
46
64
|
@out.puts "ERROR: `gh auth token` returned nothing — run `gh auth login`"
|
|
47
65
|
return false
|
|
@@ -69,7 +87,7 @@ module Inquirex
|
|
|
69
87
|
|
|
70
88
|
@out.print format(" %<name>-16s ", name: name)
|
|
71
89
|
env = { "CHANGELOG_GITHUB_TOKEN" => token, "RBENV_VERSION" => ENV.fetch("RBENV_VERSION", DEFAULT_RUBY) }
|
|
72
|
-
if
|
|
90
|
+
if @runner.call(dir, env)
|
|
73
91
|
@out.puts "ok (#{line_count(File.join(dir, "CHANGELOG.md"))} lines)"
|
|
74
92
|
true
|
|
75
93
|
else
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inquirex
|
|
4
|
+
module Tools
|
|
5
|
+
module Commands
|
|
6
|
+
# `inquirex create-release` — tags and publishes a GitHub release for
|
|
7
|
+
# every package in the family.
|
|
8
|
+
#
|
|
9
|
+
# The orchestrator runs each package's own `just release` in that
|
|
10
|
+
# package's directory, so how a repository tags and writes its notes
|
|
11
|
+
# stays that repository's business. What this adds is the order, refusing
|
|
12
|
+
# to tag a dirty tree or a feature branch, and one command instead of six.
|
|
13
|
+
#
|
|
14
|
+
# Separate from `publish` on purpose. Publishing is irreversible and
|
|
15
|
+
# authenticated; releasing is neither, and the two fail for entirely
|
|
16
|
+
# different reasons — a release that cannot find `gh` should not leave a
|
|
17
|
+
# half-published family behind it.
|
|
18
|
+
class CreateRelease < Dry::CLI::Command
|
|
19
|
+
desc "Tag and create a GitHub release for every package in the family"
|
|
20
|
+
|
|
21
|
+
option :dry_run,
|
|
22
|
+
type: :boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
desc: "Print what would run; tag nothing"
|
|
25
|
+
|
|
26
|
+
option :force,
|
|
27
|
+
type: :boolean,
|
|
28
|
+
default: false,
|
|
29
|
+
desc: "Release even from a dirty tree or a branch other than main"
|
|
30
|
+
|
|
31
|
+
example [
|
|
32
|
+
" # release every package at its current version",
|
|
33
|
+
"--dry-run # print what would happen, tag nothing",
|
|
34
|
+
"--force # skip the clean-tree and main-branch checks"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Safe to re-run: each package's recipe force-moves its tag and
|
|
38
|
+
# recreates its GitHub release, so a run that stopped halfway can be
|
|
39
|
+
# fixed and reissued rather than unpicked.
|
|
40
|
+
#
|
|
41
|
+
# @param options [Hash] parsed CLI options
|
|
42
|
+
# @return [void]
|
|
43
|
+
def call(**options)
|
|
44
|
+
ok = Inquirex::Tools::Releaser.new.call(
|
|
45
|
+
dry_run: options.fetch(:dry_run, false),
|
|
46
|
+
force: options.fetch(:force, false)
|
|
47
|
+
)
|
|
48
|
+
exit(1) unless ok
|
|
49
|
+
rescue Inquirex::Tools::Error => e
|
|
50
|
+
warn "ERROR: #{e.message}"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -17,15 +17,29 @@ module Inquirex
|
|
|
17
17
|
default: false,
|
|
18
18
|
desc: "Print the commands that would run; publish nothing"
|
|
19
19
|
|
|
20
|
+
option :force,
|
|
21
|
+
type: :boolean,
|
|
22
|
+
default: false,
|
|
23
|
+
desc: "Attempt every package, even ones already on their registry"
|
|
24
|
+
|
|
20
25
|
example [
|
|
21
|
-
" # publish
|
|
22
|
-
"--dry-run # print what would happen, touch no registry"
|
|
26
|
+
" # publish whatever is not out yet",
|
|
27
|
+
"--dry-run # print what would happen, touch no registry",
|
|
28
|
+
"--force # skip the already-published check"
|
|
23
29
|
]
|
|
24
30
|
|
|
31
|
+
# Idempotent by default: a package whose version is already on its
|
|
32
|
+
# registry is skipped, so re-running after a mid-release failure
|
|
33
|
+
# resumes rather than throwing rejections at RubyGems and npm.
|
|
34
|
+
#
|
|
25
35
|
# @param options [Hash] parsed CLI options
|
|
26
36
|
# @return [void]
|
|
27
37
|
def call(**options)
|
|
28
|
-
|
|
38
|
+
ok = Inquirex::Tools::Publisher.new.call(
|
|
39
|
+
dry_run: options.fetch(:dry_run, false),
|
|
40
|
+
force: options.fetch(:force, false)
|
|
41
|
+
)
|
|
42
|
+
exit(1) unless ok
|
|
29
43
|
rescue Inquirex::Tools::Error => e
|
|
30
44
|
warn "ERROR: #{e.message}"
|
|
31
45
|
exit 1
|
|
@@ -6,6 +6,7 @@ require_relative "commands/version"
|
|
|
6
6
|
require_relative "commands/versions_bump"
|
|
7
7
|
require_relative "commands/versions_check"
|
|
8
8
|
require_relative "commands/publish"
|
|
9
|
+
require_relative "commands/create_release"
|
|
9
10
|
require_relative "commands/changelogs"
|
|
10
11
|
|
|
11
12
|
module Inquirex
|
|
@@ -21,6 +22,9 @@ module Inquirex
|
|
|
21
22
|
register "versions-check", VersionsCheck
|
|
22
23
|
register "versions-bump", VersionsBump
|
|
23
24
|
register "publish", Publish
|
|
25
|
+
# `publish-all` reads naturally next to the justfile recipe of that name.
|
|
26
|
+
register "publish-all", Publish
|
|
27
|
+
register "create-release", CreateRelease
|
|
24
28
|
register "changelogs", Changelogs
|
|
25
29
|
register "version", Version
|
|
26
30
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "colored2"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
4
6
|
require_relative "workspace"
|
|
5
7
|
|
|
6
8
|
module Inquirex
|
|
@@ -53,39 +55,88 @@ module Inquirex
|
|
|
53
55
|
code.empty? ? nil : code
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
# Asks the registry whether a version is already public.
|
|
59
|
+
#
|
|
60
|
+
# Returns nil rather than false when the answer cannot be obtained — no
|
|
61
|
+
# network, a 500, a timeout. The caller treats nil as "attempt it": the
|
|
62
|
+
# registry itself refuses a duplicate, so guessing "not published" and
|
|
63
|
+
# trying is safe, whereas guessing "published" would silently skip a
|
|
64
|
+
# package that never shipped.
|
|
65
|
+
DEFAULT_PUBLISHED_CHECKER = lambda { |name, kind, version|
|
|
66
|
+
url = case kind
|
|
67
|
+
when :gem then "https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json"
|
|
68
|
+
when :npm then "https://registry.npmjs.org/#{name}/#{version}"
|
|
69
|
+
end
|
|
70
|
+
begin
|
|
71
|
+
uri = URI.parse(url)
|
|
72
|
+
response = Net::HTTP.start(uri.host,
|
|
73
|
+
uri.port,
|
|
74
|
+
use_ssl: true,
|
|
75
|
+
open_timeout: 5,
|
|
76
|
+
read_timeout: 5) do |http|
|
|
77
|
+
http.head(uri.request_uri)
|
|
78
|
+
end
|
|
79
|
+
case response
|
|
80
|
+
when Net::HTTPSuccess then true
|
|
81
|
+
when Net::HTTPNotFound then false
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
# @param workspace [Workspace] the ecosystem checkout to publish from
|
|
57
89
|
# @param out [IO] where report output goes
|
|
58
90
|
# @param runner [#call] invoked with (dir, argv) to run a command; the
|
|
59
91
|
# default shells out. Injected so specs can assert what would run
|
|
60
92
|
# without publishing anything.
|
|
61
93
|
# @param otp_reader [#call] returns the current 2FA code, or nil
|
|
94
|
+
# @param published_checker [#call] (name, kind, version) => true/false/nil
|
|
62
95
|
def initialize(workspace: Workspace.new,
|
|
63
96
|
out: $stdout,
|
|
64
97
|
runner: DEFAULT_RUNNER,
|
|
65
|
-
otp_reader: DEFAULT_OTP_READER
|
|
98
|
+
otp_reader: DEFAULT_OTP_READER,
|
|
99
|
+
published_checker: DEFAULT_PUBLISHED_CHECKER)
|
|
66
100
|
@workspace = workspace
|
|
67
101
|
@out = out
|
|
68
102
|
@runner = runner
|
|
69
103
|
@otp_reader = otp_reader
|
|
104
|
+
@published_checker = published_checker
|
|
70
105
|
end
|
|
71
106
|
|
|
72
107
|
# Publishes every lockstep package, or prints what it would publish.
|
|
73
108
|
#
|
|
109
|
+
# Idempotent: a package whose current version is already on its registry
|
|
110
|
+
# is skipped, so re-running after a mid-release failure picks up exactly
|
|
111
|
+
# where it stopped instead of throwing six rejections at two registries.
|
|
112
|
+
#
|
|
74
113
|
# @param dry_run [Boolean] print the commands and run nothing
|
|
75
114
|
# @param args [Array<String>] extra arguments forwarded verbatim to each
|
|
76
115
|
# package's `just publish`
|
|
77
|
-
# @
|
|
78
|
-
|
|
116
|
+
# @param force [Boolean] attempt every package even when it looks published
|
|
117
|
+
# @return [Boolean] true when every package published, skipped or printed
|
|
118
|
+
def call(dry_run: false, args: [], force: false)
|
|
79
119
|
out.puts(dry_run ? "DRY RUN — these commands would run, in this order:" : "Publishing the family:")
|
|
80
120
|
|
|
81
121
|
last_otp = nil
|
|
122
|
+
published = 0
|
|
123
|
+
skipped = 0
|
|
124
|
+
|
|
82
125
|
Workspace::LOCKSTEP.each do |name, kind|
|
|
83
126
|
dir = File.join(workspace.root, name)
|
|
84
127
|
raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)
|
|
85
128
|
|
|
129
|
+
version = workspace.version_of(name)
|
|
130
|
+
if !force && version && published_checker.call(name, kind, version)
|
|
131
|
+
row(name, kind, "already #{version} on #{registry(kind)} — skipping".dark)
|
|
132
|
+
skipped += 1
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
|
|
86
136
|
argv = ["publish", *args]
|
|
87
137
|
if dry_run
|
|
88
|
-
row(name, kind, "just #{argv.join(" ")}")
|
|
138
|
+
row(name, kind, "just #{argv.join(" ")}#{" (#{version})" if version}")
|
|
139
|
+
published += 1
|
|
89
140
|
next
|
|
90
141
|
end
|
|
91
142
|
|
|
@@ -98,10 +149,12 @@ module Inquirex
|
|
|
98
149
|
out.puts
|
|
99
150
|
out.puts "━━ #{name} (#{kind}) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
100
151
|
return false unless run(name, dir, argv)
|
|
152
|
+
|
|
153
|
+
published += 1
|
|
101
154
|
end
|
|
102
155
|
|
|
103
156
|
out.puts
|
|
104
|
-
out.puts(dry_run
|
|
157
|
+
out.puts summary(dry_run:, published:, skipped:)
|
|
105
158
|
true
|
|
106
159
|
end
|
|
107
160
|
|
|
@@ -119,6 +172,9 @@ module Inquirex
|
|
|
119
172
|
# @return [#call] source of 2FA codes
|
|
120
173
|
attr_reader :otp_reader
|
|
121
174
|
|
|
175
|
+
# @return [#call] asks a registry whether a version is already public
|
|
176
|
+
attr_reader :published_checker
|
|
177
|
+
|
|
122
178
|
# @return [Boolean] whether the package published cleanly
|
|
123
179
|
def run(name, dir, argv)
|
|
124
180
|
return true if runner.call(dir, argv)
|
|
@@ -155,6 +211,19 @@ module Inquirex
|
|
|
155
211
|
def row(name, kind, detail)
|
|
156
212
|
out.puts " #{name.ljust(18).yellow} #{kind.to_s.ljust(4).cyan} #{detail}"
|
|
157
213
|
end
|
|
214
|
+
|
|
215
|
+
# @return [String] human-readable registry name for a package kind
|
|
216
|
+
def registry(kind) = kind == :gem ? "RubyGems" : "npm"
|
|
217
|
+
|
|
218
|
+
# @return [String] the closing line, which has to distinguish "nothing to
|
|
219
|
+
# do because everything is already out" from "nothing happened"
|
|
220
|
+
def summary(dry_run:, published:, skipped:)
|
|
221
|
+
return "Everything is already published — nothing to do.".green if published.zero? && skipped.positive?
|
|
222
|
+
|
|
223
|
+
counted = "#{published} package#{"s" unless published == 1}"
|
|
224
|
+
note = skipped.positive? ? ", #{skipped} already published" : ""
|
|
225
|
+
dry_run ? "Would publish #{counted}#{note}. Nothing was published." : "Published #{counted}#{note}.".green
|
|
226
|
+
end
|
|
158
227
|
end
|
|
159
228
|
end
|
|
160
229
|
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colored2"
|
|
4
|
+
require_relative "workspace"
|
|
5
|
+
|
|
6
|
+
module Inquirex
|
|
7
|
+
module Tools
|
|
8
|
+
# Cuts a GitHub release for every package in the family by running each
|
|
9
|
+
# one's own `just release` in its own directory.
|
|
10
|
+
#
|
|
11
|
+
# The counterpart to publishing: that puts packages on RubyGems and npm,
|
|
12
|
+
# this puts a `vX.Y.Z` tag and a GitHub release against the commit that
|
|
13
|
+
# shipped. Every package already owns a `release` recipe — it tags,
|
|
14
|
+
# force-pushes the tag, and recreates the GitHub release with generated
|
|
15
|
+
# notes — so how a repository releases stays that repository's business.
|
|
16
|
+
# What this adds is the order, the guards, and one command instead of six.
|
|
17
|
+
#
|
|
18
|
+
# Unlike publishing, a release is **reversible**: a tag can be moved and a
|
|
19
|
+
# GitHub release deleted, which is exactly what each recipe does on every
|
|
20
|
+
# run. That is why this is safe to re-run, and why a mid-run failure can be
|
|
21
|
+
# fixed and the command simply issued again.
|
|
22
|
+
#
|
|
23
|
+
# The guards exist because the recipe tags `HEAD`. Releasing from a dirty
|
|
24
|
+
# tree tags a commit that does not match what was tested; releasing from a
|
|
25
|
+
# feature branch tags work that was never merged. Both produce a `v0.9.5`
|
|
26
|
+
# pointing somewhere nobody can reproduce, and neither is fixed by
|
|
27
|
+
# re-running — so they are refused rather than warned about.
|
|
28
|
+
#
|
|
29
|
+
# @example Preview a release without tagging anything
|
|
30
|
+
# Inquirex::Tools::Releaser.new.call(dry_run: true)
|
|
31
|
+
class Releaser
|
|
32
|
+
# The only branch a release may be cut from. A tag on anything else
|
|
33
|
+
# points at work that never went through review.
|
|
34
|
+
RELEASE_BRANCH = "main"
|
|
35
|
+
|
|
36
|
+
# Shells out with the child's stdout and stderr left attached, so a
|
|
37
|
+
# failing package's own `gh` output is what the operator sees.
|
|
38
|
+
DEFAULT_RUNNER = lambda { |dir, argv|
|
|
39
|
+
Dir.chdir(dir) { system("just", *argv) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# @param workspace [Workspace] the ecosystem checkout to release from
|
|
43
|
+
# @param out [IO] where report output goes
|
|
44
|
+
# @param runner [#call] invoked with (dir, argv) to run a command; the
|
|
45
|
+
# default shells out. Injected so specs can assert what would run
|
|
46
|
+
# without creating a tag or a GitHub release.
|
|
47
|
+
def initialize(workspace: Workspace.new, out: $stdout, runner: DEFAULT_RUNNER)
|
|
48
|
+
@workspace = workspace
|
|
49
|
+
@out = out
|
|
50
|
+
@runner = runner
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Creates a GitHub release for every lockstep package.
|
|
54
|
+
#
|
|
55
|
+
# Stops at the first package that fails or is refused, and says so. The
|
|
56
|
+
# packages after it are left alone rather than half-tagged.
|
|
57
|
+
#
|
|
58
|
+
# @param dry_run [Boolean] print what would run; tag nothing
|
|
59
|
+
# @param force [Boolean] release despite a dirty tree or a non-main branch
|
|
60
|
+
# @param args [Array<String>] extra arguments forwarded to `just release`
|
|
61
|
+
# @return [Boolean] true when every package released or was printed
|
|
62
|
+
def call(dry_run: false, force: false, args: [])
|
|
63
|
+
out.puts(dry_run ? "DRY RUN — these releases would be created:" : "Creating GitHub releases for the family:")
|
|
64
|
+
|
|
65
|
+
released = 0
|
|
66
|
+
|
|
67
|
+
Workspace::LOCKSTEP.each_key do |name|
|
|
68
|
+
dir = File.join(workspace.root, name)
|
|
69
|
+
raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)
|
|
70
|
+
|
|
71
|
+
version = workspace.version_of(name)
|
|
72
|
+
|
|
73
|
+
blocker = force ? nil : blocker_for(name)
|
|
74
|
+
if blocker
|
|
75
|
+
row(name, version, blocker.red)
|
|
76
|
+
return refused(name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
argv = ["release", *args]
|
|
80
|
+
if dry_run
|
|
81
|
+
row(name, version, "just #{argv.join(" ")}")
|
|
82
|
+
released += 1
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
out.puts
|
|
87
|
+
out.puts "━━ #{name} v#{version} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
88
|
+
return false unless run(name, dir, argv)
|
|
89
|
+
|
|
90
|
+
released += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
out.puts
|
|
94
|
+
out.puts summary(dry_run:, released:)
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# @return [Workspace] the ecosystem checkout being released
|
|
101
|
+
attr_reader :workspace
|
|
102
|
+
|
|
103
|
+
# @return [IO] where report output goes
|
|
104
|
+
attr_reader :out
|
|
105
|
+
|
|
106
|
+
# @return [#call] command runner
|
|
107
|
+
attr_reader :runner
|
|
108
|
+
|
|
109
|
+
# Why this package must not be released, or nil when it is fine.
|
|
110
|
+
#
|
|
111
|
+
# A `clean` of nil means the directory is not a git checkout at all. That
|
|
112
|
+
# is left for the recipe to fail on rather than pre-judged here: a state
|
|
113
|
+
# that cannot be read should never silently skip a package, because
|
|
114
|
+
# "unknown" and "not releasable" are different answers.
|
|
115
|
+
#
|
|
116
|
+
# @param name [String] package name
|
|
117
|
+
# @return [String, nil] human-readable blocker
|
|
118
|
+
def blocker_for(name)
|
|
119
|
+
state = workspace.git_state(name)
|
|
120
|
+
return "working tree is dirty — the tag would not match what was tested" if state[:clean] == false
|
|
121
|
+
|
|
122
|
+
branch = state[:branch]
|
|
123
|
+
return nil if branch.nil? || branch.empty? || branch == RELEASE_BRANCH
|
|
124
|
+
|
|
125
|
+
"on #{branch}, not #{RELEASE_BRANCH} — the tag would point at unmerged work"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param name [String] the package that was refused
|
|
129
|
+
# @return [Boolean] always false, so the caller can `return refused(name)`
|
|
130
|
+
def refused(name)
|
|
131
|
+
out.puts
|
|
132
|
+
out.puts "#{"ERROR".bold.red}: #{name} is not releasable — stopping here."
|
|
133
|
+
out.puts "Nothing was tagged. Fix the checkout, or re-run with --force to override."
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [Boolean] whether the package released cleanly
|
|
138
|
+
def run(name, dir, argv)
|
|
139
|
+
return true if runner.call(dir, argv)
|
|
140
|
+
|
|
141
|
+
out.puts
|
|
142
|
+
out.puts "#{"ERROR".bold.red}: #{name} failed `just #{argv.first}` — stopping here."
|
|
143
|
+
out.puts "A release is re-runnable, so fix the cause and issue the command again."
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Pads before colorizing: an ANSI escape is several characters wide to
|
|
148
|
+
# `String#ljust` and zero columns wide on screen, so colorizing first left
|
|
149
|
+
# the table ragged by exactly the length of the escape codes.
|
|
150
|
+
#
|
|
151
|
+
# @return [void]
|
|
152
|
+
def row(name, version, detail)
|
|
153
|
+
out.puts " #{name.ljust(18).yellow} #{(version || "?").ljust(8).cyan} #{detail}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [String] the closing line
|
|
157
|
+
def summary(dry_run:, released:)
|
|
158
|
+
counted = "#{released} release#{"s" unless released == 1}"
|
|
159
|
+
dry_run ? "Would create #{counted}. Nothing was tagged." : "Created #{counted}.".green
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "colored2"
|
|
4
|
+
require "open3"
|
|
4
5
|
require "rubygems/version"
|
|
5
6
|
require_relative "workspace"
|
|
6
7
|
|
|
@@ -37,11 +38,43 @@ module Inquirex
|
|
|
37
38
|
# Rule across the report, sized to the rows.
|
|
38
39
|
RULE = ("—" * 52).freeze
|
|
39
40
|
|
|
41
|
+
# Bringing a checkout level with origin/main before its suite runs, so a
|
|
42
|
+
# bump is verified against what is actually on main rather than whatever
|
|
43
|
+
# the local clone last saw.
|
|
44
|
+
SYNC_STEPS = [
|
|
45
|
+
%w[fetch --tags origin],
|
|
46
|
+
%w[rebase origin/main]
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
# Runs one git command, capturing stdout *and* stderr.
|
|
50
|
+
#
|
|
51
|
+
# Capturing is the point. Left attached, a `git` run against a directory
|
|
52
|
+
# that is not a checkout writes "fatal: not a git repository" straight to
|
|
53
|
+
# the terminal — thirty such lines during the specs, which is noise the
|
|
54
|
+
# operator has to learn to ignore. The caller decides what is worth
|
|
55
|
+
# printing.
|
|
56
|
+
#
|
|
57
|
+
# @return [Array(Boolean, String)] success, and the combined output
|
|
58
|
+
DEFAULT_GIT = lambda { |dir, argv|
|
|
59
|
+
output, status = Open3.capture2e("git", "-C", dir, *argv)
|
|
60
|
+
[status.success?, output]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Runs a package's own checks in its own directory.
|
|
64
|
+
DEFAULT_RUNNER = ->(dir, argv) { Dir.chdir(dir) { system(*argv) } }
|
|
65
|
+
|
|
40
66
|
# @param workspace [Workspace] the ecosystem checkout to rewrite
|
|
41
67
|
# @param out [IO] where report output goes
|
|
42
|
-
|
|
68
|
+
# @param git [#call] invoked with (dir, argv); returns [ok, output].
|
|
69
|
+
# Injected so specs can assert what would run without touching a repo.
|
|
70
|
+
# @param runner [#call] invoked with (dir, argv) to run a package's
|
|
71
|
+
# checks. Injected for the same reason: a unit test must not shell out
|
|
72
|
+
# to six real suites.
|
|
73
|
+
def initialize(workspace: Workspace.new, out: $stdout, git: DEFAULT_GIT, runner: DEFAULT_RUNNER)
|
|
43
74
|
@workspace = workspace
|
|
44
75
|
@out = out
|
|
76
|
+
@git = git
|
|
77
|
+
@runner = runner
|
|
45
78
|
end
|
|
46
79
|
|
|
47
80
|
# Rewrites every lockstep package to the target version, then runs each
|
|
@@ -74,6 +107,12 @@ module Inquirex
|
|
|
74
107
|
# @return [IO] where report output goes
|
|
75
108
|
attr_reader :out
|
|
76
109
|
|
|
110
|
+
# @return [#call] runs one git command, returning [ok, output]
|
|
111
|
+
attr_reader :git
|
|
112
|
+
|
|
113
|
+
# @return [#call] runs a package's checks, returning truthy on success
|
|
114
|
+
attr_reader :runner
|
|
115
|
+
|
|
77
116
|
# The one place the package list is walked, so adding a package changes
|
|
78
117
|
# nothing here.
|
|
79
118
|
#
|
|
@@ -100,25 +139,47 @@ module Inquirex
|
|
|
100
139
|
# @return [Boolean, nil] pass/fail, or nil when skipped
|
|
101
140
|
def check(name)
|
|
102
141
|
dir = File.join(workspace.root, name)
|
|
103
|
-
|
|
104
|
-
row(name, "
|
|
105
|
-
|
|
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)
|
|
142
|
+
unless File.exist?(File.join(dir, "justfile"))
|
|
143
|
+
row(name, "skip".yellow, "no justfile".yellow)
|
|
144
|
+
return nil
|
|
117
145
|
end
|
|
118
|
-
|
|
146
|
+
|
|
147
|
+
sync(name, dir)
|
|
148
|
+
passed = runner.call(dir, CHECK)
|
|
149
|
+
row(name, passed ? "ok".green : "FAIL".red, dir)
|
|
119
150
|
passed
|
|
120
151
|
end
|
|
121
152
|
|
|
153
|
+
# Brings one package level with origin/main before its suite runs.
|
|
154
|
+
#
|
|
155
|
+
# Two states are reported and left alone rather than acted on:
|
|
156
|
+
#
|
|
157
|
+
# - **not a git checkout** — nothing to sync, and running git here is what
|
|
158
|
+
# printed "fatal: not a git repository" once per command per package.
|
|
159
|
+
# - **uncommitted changes** — the tree is *not* stashed. A version bump
|
|
160
|
+
# that silently runs `git stash -u` can bury work the operator never
|
|
161
|
+
# agreed to move, and the stash stack is shared across worktrees, so it
|
|
162
|
+
# is not even reliably theirs to pop back. Rebasing on top of local
|
|
163
|
+
# edits is equally unwelcome. Skipping is the only non-destructive
|
|
164
|
+
# answer; the checks still run against what is on disk.
|
|
165
|
+
#
|
|
166
|
+
# @param name [String] package name
|
|
167
|
+
# @param dir [String] absolute path to the package
|
|
168
|
+
# @return [void]
|
|
169
|
+
def sync(name, dir)
|
|
170
|
+
state = workspace.git_state(name)
|
|
171
|
+
return row(name, "sync".yellow, "not a git checkout".yellow) if state[:clean].nil?
|
|
172
|
+
return row(name, "sync".yellow, "uncommitted changes — left alone".yellow) unless state[:clean]
|
|
173
|
+
|
|
174
|
+
SYNC_STEPS.each do |argv|
|
|
175
|
+
ok, output = git.call(dir, argv)
|
|
176
|
+
next if ok
|
|
177
|
+
|
|
178
|
+
row(name, "sync".red, "git #{argv.first} failed: #{output.to_s.lines.first&.strip}".red)
|
|
179
|
+
break
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
122
183
|
# The single formatting site for every line of the report.
|
|
123
184
|
#
|
|
124
185
|
# `format`, never `printf`: printf writes straight to $stdout and returns
|
|
@@ -42,7 +42,10 @@ module Inquirex
|
|
|
42
42
|
|
|
43
43
|
Workspace::LOCKSTEP.each_key do |name|
|
|
44
44
|
problems = readiness_problems(name, target)
|
|
45
|
-
|
|
45
|
+
# `.green`, not `.bright_green`: colored2 defines no such method, so
|
|
46
|
+
# the ready branch raised NoMethodError — and only the ready branch,
|
|
47
|
+
# which is why a preflight that found problems looked fine.
|
|
48
|
+
out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready".green : problems.join(", ").red)
|
|
46
49
|
ok &&= problems.empty?
|
|
47
50
|
end
|
|
48
51
|
|
data/lib/inquirex/tools.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "tools/workspace"
|
|
|
5
5
|
require_relative "tools/version_checker"
|
|
6
6
|
require_relative "tools/version_bumper"
|
|
7
7
|
require_relative "tools/publisher"
|
|
8
|
+
require_relative "tools/releaser"
|
|
8
9
|
require_relative "tools/changelogs"
|
|
9
10
|
|
|
10
11
|
module Inquirex
|
|
@@ -24,6 +25,7 @@ module Inquirex
|
|
|
24
25
|
# - `versions-check` — version parity across the lockstep packages
|
|
25
26
|
# - `versions-bump` — move every lockstep package to one version
|
|
26
27
|
# - `publish` — publish every package to RubyGems and npm
|
|
28
|
+
# - `create-release` — tag and create a GitHub release for every package
|
|
27
29
|
# - `changelogs` — regenerate CHANGELOG.md from merged PRs
|
|
28
30
|
# - `version` — this gem's own version
|
|
29
31
|
#
|
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.9.
|
|
4
|
+
version: 0.9.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -63,6 +63,8 @@ files:
|
|
|
63
63
|
- LICENSE.txt
|
|
64
64
|
- README.md
|
|
65
65
|
- Rakefile
|
|
66
|
+
- docs/badges/coverage_badge.svg
|
|
67
|
+
- docs/monorepo-migration.md
|
|
66
68
|
- exe/inquirex
|
|
67
69
|
- exe/inquirexor
|
|
68
70
|
- justfile
|
|
@@ -70,11 +72,13 @@ files:
|
|
|
70
72
|
- lib/inquirex/tools/changelogs.rb
|
|
71
73
|
- lib/inquirex/tools/commands.rb
|
|
72
74
|
- lib/inquirex/tools/commands/changelogs.rb
|
|
75
|
+
- lib/inquirex/tools/commands/create_release.rb
|
|
73
76
|
- lib/inquirex/tools/commands/publish.rb
|
|
74
77
|
- lib/inquirex/tools/commands/version.rb
|
|
75
78
|
- lib/inquirex/tools/commands/versions_bump.rb
|
|
76
79
|
- lib/inquirex/tools/commands/versions_check.rb
|
|
77
80
|
- lib/inquirex/tools/publisher.rb
|
|
81
|
+
- lib/inquirex/tools/releaser.rb
|
|
78
82
|
- lib/inquirex/tools/version.rb
|
|
79
83
|
- lib/inquirex/tools/version_bumper.rb
|
|
80
84
|
- lib/inquirex/tools/version_checker.rb
|