hecks 2.6.0 → 2.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 +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require_relative "gem_pin/published_versions"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Release
|
|
5
|
+
# Finds the hecks version a project runs on, and refuses one that a
|
|
6
|
+
# stranger could not install.
|
|
7
|
+
#
|
|
8
|
+
# ## Why it exists
|
|
9
|
+
#
|
|
10
|
+
# A project that is copied out of its own workspace (handed to someone who
|
|
11
|
+
# will `bundle install` it, or built in an image with no sibling checkout)
|
|
12
|
+
# has to name a hecks release RubyGems actually publishes. A `path:` or
|
|
13
|
+
# `git:` source only works where the path or remote is reachable, and a
|
|
14
|
+
# version that was never published resolves nowhere. This reads the
|
|
15
|
+
# project's Gemfiles and lockfiles, without running Bundler, and says which
|
|
16
|
+
# version each pins or refuses.
|
|
17
|
+
#
|
|
18
|
+
# ## Resolution
|
|
19
|
+
#
|
|
20
|
+
# For each `Gemfile` under the root (a project can hold several, such as a
|
|
21
|
+
# domain and a helper app) the pin is, in order: the version its
|
|
22
|
+
# `Gemfile.lock` resolved, or the newest stable release its version
|
|
23
|
+
# constraint allows. Each pin must be published; the Gemfiles need not
|
|
24
|
+
# agree.
|
|
25
|
+
#
|
|
26
|
+
# ## Injectable network
|
|
27
|
+
#
|
|
28
|
+
# The published list comes from `published:`, any object answering
|
|
29
|
+
# `include?(version)` and `newest_satisfying(requirement)`. The default asks
|
|
30
|
+
# RubyGems; a caller without network hands in its own.
|
|
31
|
+
class GemPin
|
|
32
|
+
GEM_LINE = /^\s*gem\s+["']hecks["'](?<rest>.*)$/
|
|
33
|
+
SOURCE_OPTION = /\b(?:path|git|github|gitlab|bitbucket|branch|ref|tag):/
|
|
34
|
+
LOCKED = /^ hecks \((?<version>[^)]+)\)/
|
|
35
|
+
|
|
36
|
+
# What `resolve` found.
|
|
37
|
+
#
|
|
38
|
+
# @!attribute [r] version
|
|
39
|
+
# @return [String] the newest pin
|
|
40
|
+
# @!attribute [r] pins
|
|
41
|
+
# @return [Hash{String => String}] each Gemfile that declares hecks, relative to the
|
|
42
|
+
# root, mapped to the version it runs on
|
|
43
|
+
Resolved = Struct.new(:version, :pins, keyword_init: true)
|
|
44
|
+
|
|
45
|
+
# @param published [#include?, #newest_satisfying] the source of published versions
|
|
46
|
+
def initialize(published: PublishedVersions.new)
|
|
47
|
+
@published = published
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves the hecks versions the project in `root` runs on.
|
|
51
|
+
#
|
|
52
|
+
# @param root [String] the project's root directory
|
|
53
|
+
# @return [Resolved] the newest pin and every pin
|
|
54
|
+
# @raise [Error] if no Gemfile declares hecks, one takes it from a source other than
|
|
55
|
+
# RubyGems, one pins a version RubyGems does not publish, or a constraint allows
|
|
56
|
+
# no published release
|
|
57
|
+
def resolve(root)
|
|
58
|
+
pins = gemfiles(root).filter_map { |gemfile| pin_for(gemfile, root) }.to_h
|
|
59
|
+
raise Error, "no Gemfile declares the hecks gem, so this is not a Hecks project" if pins.empty?
|
|
60
|
+
|
|
61
|
+
pins.each do |rel, version|
|
|
62
|
+
raise Error, "hecks #{version} is not published on RubyGems (#{rel})" unless @published.include?(version)
|
|
63
|
+
end
|
|
64
|
+
Resolved.new(version: pins.values.max_by { |version| Gem::Version.new(version) }, pins: pins)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def gemfiles(root)
|
|
70
|
+
Dir.glob("**/Gemfile", base: root).reject { |rel| rel.include?("node_modules/") }.sort
|
|
71
|
+
.map { |rel| File.join(root, rel) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pin_for(gemfile, root)
|
|
75
|
+
declaration = File.read(gemfile).match(GEM_LINE) or return nil
|
|
76
|
+
rel = gemfile.delete_prefix("#{root}/")
|
|
77
|
+
if (source = declaration[:rest][SOURCE_OPTION])
|
|
78
|
+
raise Error, "#{rel} takes hecks from `#{source}`, not from RubyGems; the project must use a published version"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[rel, locked_version(gemfile, rel) || newest_allowed(declaration[:rest], rel)]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The version Gemfile.lock resolved, if there is a lockfile. A hecks
|
|
85
|
+
# entry under `PATH` or `GIT` means the lockfile is not installable either.
|
|
86
|
+
def locked_version(gemfile, rel)
|
|
87
|
+
lock = "#{gemfile}.lock"
|
|
88
|
+
return nil unless File.exist?(lock)
|
|
89
|
+
|
|
90
|
+
text = File.read(lock)
|
|
91
|
+
refuse_unpublished_source!(text, rel)
|
|
92
|
+
text.match(LOCKED)&.[](:version)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def refuse_unpublished_source!(lock_text, rel)
|
|
96
|
+
lock_text.split("\n\n").each do |section|
|
|
97
|
+
next unless section.start_with?("PATH", "GIT") && section.match?(LOCKED)
|
|
98
|
+
|
|
99
|
+
raise Error, "#{rel}.lock resolves hecks from a #{section.lines.first.strip} source, not from RubyGems"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def newest_allowed(rest, rel)
|
|
104
|
+
constraints = rest.scan(/["']([^"']+)["']/).flatten.grep(/\A\s*(?:[~><=!]+\s*)?\d/)
|
|
105
|
+
version = @published.newest_satisfying(Gem::Requirement.new(*constraints))
|
|
106
|
+
raise Error, "no published hecks release satisfies the constraint in #{rel}" unless version
|
|
107
|
+
|
|
108
|
+
version.to_s
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require_relative "commands"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Release
|
|
5
|
+
class Runner
|
|
6
|
+
# The npm step when CI publishes: the release tag triggers
|
|
7
|
+
# `.github/workflows/publish-client.yml`, which publishes `@hecks/client`
|
|
8
|
+
# with npm trusted publishing, so nothing is published from this machine.
|
|
9
|
+
# This watches npm until the version appears.
|
|
10
|
+
#
|
|
11
|
+
# The clock and the pause are injected, so a spec runs the whole wait
|
|
12
|
+
# without sleeping.
|
|
13
|
+
class CiPublisher
|
|
14
|
+
WORKFLOW = ".github/workflows/publish-client.yml".freeze
|
|
15
|
+
INTERVAL = 15
|
|
16
|
+
LIMIT = 600
|
|
17
|
+
|
|
18
|
+
# @param root [String] the repository root
|
|
19
|
+
# @param published [Published] asks npm whether the version is out
|
|
20
|
+
# @param console [Console] progress output
|
|
21
|
+
# @param pause [#call] sleeps for a number of seconds
|
|
22
|
+
# @param now [#call] answers the current time in seconds, monotonically
|
|
23
|
+
def initialize(root:, published:, console:, pause:, now:)
|
|
24
|
+
@root = root
|
|
25
|
+
@published = published
|
|
26
|
+
@console = console
|
|
27
|
+
@pause = pause
|
|
28
|
+
@now = now
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Says whether the checkout carries the workflow that publishes the client.
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] true when `.github/workflows/publish-client.yml` exists
|
|
34
|
+
def workflow?
|
|
35
|
+
File.exist?(File.join(@root, WORKFLOW))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Tells the person CI publishes, then waits for the version to reach npm.
|
|
39
|
+
#
|
|
40
|
+
# @param version [String] the version being released
|
|
41
|
+
# @param dry_run [Boolean] say what would happen and wait for nothing
|
|
42
|
+
# @param wait [Boolean] false to report and return without polling
|
|
43
|
+
# @return [Boolean] false only when the wait timed out; true when the version appeared or
|
|
44
|
+
# nothing was waited for
|
|
45
|
+
def wait!(version, dry_run:, wait:)
|
|
46
|
+
@console.say("CI publishes @hecks/client #{version} from the tag (#{WORKFLOW}); nothing is published from here.")
|
|
47
|
+
return true.tap { preview } if dry_run
|
|
48
|
+
return true.tap { skip_wait(version) } unless wait
|
|
49
|
+
|
|
50
|
+
@console.say("Waiting for @hecks/client #{version} on npm (every #{INTERVAL}s, up to #{LIMIT / 60} minutes)...")
|
|
51
|
+
arrived = arrives?(version)
|
|
52
|
+
@console.say("@hecks/client #{version} is on npm.") if arrived
|
|
53
|
+
arrived
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The hint printed when the wait ends without the version on npm.
|
|
57
|
+
#
|
|
58
|
+
# @param version [String] the version being released
|
|
59
|
+
# @return [String] how to look at, re-run and fall back from the CI publish
|
|
60
|
+
def self.timeout_hint(version)
|
|
61
|
+
"Timed out after #{LIMIT / 60} minutes waiting for CI to publish @hecks/client #{version}. " \
|
|
62
|
+
"See the run: gh run list --workflow publish-client.yml. Re-run it: " \
|
|
63
|
+
"gh workflow run publish-client.yml -f tag=v#{version}. Then finish with: bin/release --npm-only. " \
|
|
64
|
+
"To publish from this machine instead: bin/release --npm-only --npm-local."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def preview
|
|
70
|
+
@console.say("Dry run: would wait up to #{LIMIT / 60} minutes for it to appear on npm.")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def skip_wait(version)
|
|
74
|
+
@console.say("Not waiting (--no-wait). Check the run: gh run list --workflow publish-client.yml; " \
|
|
75
|
+
"npm view @hecks/client@#{version} version")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def arrives?(version)
|
|
79
|
+
deadline = @now.call + LIMIT
|
|
80
|
+
loop do
|
|
81
|
+
return true if out?(version)
|
|
82
|
+
return false if @now.call >= deadline
|
|
83
|
+
|
|
84
|
+
@pause.call(INTERVAL)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def out?(version)
|
|
89
|
+
@published.npm?(version)
|
|
90
|
+
rescue Refusal
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Release
|
|
5
|
+
class Runner
|
|
6
|
+
# Raised when a command a release step runs exits non-zero or cannot start.
|
|
7
|
+
class CommandFailed < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised when a check refuses the release; the message names the fix.
|
|
10
|
+
class Refusal < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Runs the release's external commands: git, gem, npm, curl, op.
|
|
13
|
+
#
|
|
14
|
+
# The runner takes any object answering `capture` and `run!` the way this
|
|
15
|
+
# one does, so a spec hands in a recorder and never starts a real process.
|
|
16
|
+
class Commands
|
|
17
|
+
# What a captured command printed and whether it succeeded.
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [r] stdout
|
|
20
|
+
# @return [String] standard output
|
|
21
|
+
# @!attribute [r] stderr
|
|
22
|
+
# @return [String] standard error, or the reason the command could not start
|
|
23
|
+
# @!attribute [r] success
|
|
24
|
+
# @return [Boolean] true when the command exited zero
|
|
25
|
+
Result = Struct.new(:stdout, :stderr, :success, keyword_init: true) do
|
|
26
|
+
# Says whether the command exited zero.
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true when it did
|
|
29
|
+
def success?
|
|
30
|
+
success
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Runs a command and returns what it printed, whatever its exit status.
|
|
35
|
+
#
|
|
36
|
+
# @param argv [Array<String>] the command and its arguments, never a shell string
|
|
37
|
+
# @param env [Hash{String => String, nil}] variables to set, or unset with nil
|
|
38
|
+
# @param chdir [String, nil] the directory to run in; the current one when nil
|
|
39
|
+
# @return [Result] the output and status; a command that cannot start is a failed result
|
|
40
|
+
def capture(*argv, env: {}, chdir: nil)
|
|
41
|
+
stdout, stderr, status = Open3.capture3(env, *argv, **directory(chdir))
|
|
42
|
+
Result.new(stdout: stdout, stderr: stderr, success: status.success?)
|
|
43
|
+
rescue SystemCallError => e
|
|
44
|
+
Result.new(stdout: "", stderr: e.message, success: false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Runs a command with its output going straight to the terminal.
|
|
48
|
+
#
|
|
49
|
+
# @param argv [Array<String>] the command and its arguments, never a shell string
|
|
50
|
+
# @param env [Hash{String => String, nil}] variables to set, or unset with nil
|
|
51
|
+
# @param chdir [String, nil] the directory to run in; the current one when nil
|
|
52
|
+
# @return [true] when the command exits zero
|
|
53
|
+
# @raise [CommandFailed] if the command exits non-zero or cannot start
|
|
54
|
+
def run!(*argv, env: {}, chdir: nil)
|
|
55
|
+
return true if system(env, *argv, **directory(chdir))
|
|
56
|
+
|
|
57
|
+
raise CommandFailed, "`#{argv.first(2).join(' ')}` failed"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def directory(chdir)
|
|
63
|
+
chdir ? { chdir: chdir } : {}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Release
|
|
3
|
+
class Runner
|
|
4
|
+
# Talks to the person running the release: progress lines, warnings and the
|
|
5
|
+
# y/N questions that come before anything irreversible.
|
|
6
|
+
class Console
|
|
7
|
+
# @param input [IO] where answers are read from
|
|
8
|
+
# @param out [IO] where progress goes
|
|
9
|
+
# @param err [IO] where refusals and failures go
|
|
10
|
+
# @param assume_yes [Boolean] answer every question yes without asking
|
|
11
|
+
def initialize(input:, out:, err:, assume_yes: false)
|
|
12
|
+
@input = input
|
|
13
|
+
@out = out
|
|
14
|
+
@err = err
|
|
15
|
+
@assume_yes = assume_yes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Prints a progress line.
|
|
19
|
+
#
|
|
20
|
+
# @param text [String] the line
|
|
21
|
+
# @return [void]
|
|
22
|
+
def say(text)
|
|
23
|
+
@out.puts(text)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Prints a refusal or failure line.
|
|
27
|
+
#
|
|
28
|
+
# @param text [String] the line
|
|
29
|
+
# @return [void]
|
|
30
|
+
def warn(text)
|
|
31
|
+
@err.puts(text)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Asks a yes/no question, defaulting to no.
|
|
35
|
+
#
|
|
36
|
+
# @param question [String] what will happen if the answer is yes
|
|
37
|
+
# @return [Boolean] true for `y` or `yes`, or always under `assume_yes`; false on anything
|
|
38
|
+
# else, including a closed input
|
|
39
|
+
def confirm?(question)
|
|
40
|
+
if @assume_yes
|
|
41
|
+
say("#{question} yes (--yes)")
|
|
42
|
+
return true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@out.print("#{question} [y/N] ")
|
|
46
|
+
@out.flush
|
|
47
|
+
%w[y yes].include?(@input.gets.to_s.strip.downcase)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "commands"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Release
|
|
6
|
+
class Runner
|
|
7
|
+
# The gem step: hands the push to `bin/release_gem`, which owns building the
|
|
8
|
+
# gem and pushing it with the key 1Password holds. A dry run builds the gem
|
|
9
|
+
# to prove it builds and deletes it.
|
|
10
|
+
class GemPublisher
|
|
11
|
+
# @param root [String] the repository root
|
|
12
|
+
# @param commands [#run!] runs the build and the push
|
|
13
|
+
# @param console [Console] progress output
|
|
14
|
+
def initialize(root:, commands:, console:)
|
|
15
|
+
@root = root
|
|
16
|
+
@commands = commands
|
|
17
|
+
@console = console
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Pushes the gem, or with `dry_run` only builds it.
|
|
21
|
+
#
|
|
22
|
+
# @param version [String] the version being released
|
|
23
|
+
# @param dry_run [Boolean] build and delete the gem, pushing nothing
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [CommandFailed] if the build or the push fails
|
|
26
|
+
def publish!(version, dry_run:)
|
|
27
|
+
return build_only(version) if dry_run
|
|
28
|
+
|
|
29
|
+
@console.say("Publishing hecks #{version} to rubygems.org via bin/release_gem...")
|
|
30
|
+
@commands.run!(File.join(@root, "bin/release_gem"), chdir: @root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_only(version)
|
|
36
|
+
@console.say("Dry run: building hecks-#{version}.gem (nothing is pushed)...")
|
|
37
|
+
@commands.run!("gem", "build", "hecks.gemspec", chdir: @root)
|
|
38
|
+
ensure
|
|
39
|
+
FileUtils.rm_f(File.join(@root, "hecks-#{version}.gem"))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "hecks/vendoring/git_environment"
|
|
2
|
+
require_relative "commands"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Release
|
|
6
|
+
class Runner
|
|
7
|
+
# The release's view of the repository: git run in the checkout, with the
|
|
8
|
+
# repository-pinning environment variables a git hook exports unset.
|
|
9
|
+
class Git
|
|
10
|
+
# @param root [String] the repository root
|
|
11
|
+
# @param commands [#capture, #run!] runs the git commands
|
|
12
|
+
def initialize(root:, commands:)
|
|
13
|
+
@root = root
|
|
14
|
+
@commands = commands
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Runs a git command and returns what it printed.
|
|
18
|
+
#
|
|
19
|
+
# @param args [Array<String>] git's arguments
|
|
20
|
+
# @return [String] standard output
|
|
21
|
+
# @raise [Refusal] if git exits non-zero
|
|
22
|
+
def read(*args)
|
|
23
|
+
result = capture(*args)
|
|
24
|
+
raise Refusal, "git #{args.first} failed: #{result.stderr.strip}" unless result.success?
|
|
25
|
+
|
|
26
|
+
result.stdout
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Runs a git command and returns the result whatever its status.
|
|
30
|
+
#
|
|
31
|
+
# @param args [Array<String>] git's arguments
|
|
32
|
+
# @return [Commands::Result] output and status
|
|
33
|
+
def capture(*)
|
|
34
|
+
@commands.capture("git", *, env: Vendoring::GitEnvironment.clean, chdir: @root)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Runs a git command that changes something, showing its output.
|
|
38
|
+
#
|
|
39
|
+
# @param args [Array<String>] git's arguments
|
|
40
|
+
# @return [true] when git exits zero
|
|
41
|
+
# @raise [CommandFailed] if git exits non-zero
|
|
42
|
+
def run!(*)
|
|
43
|
+
@commands.run!("git", *, env: Vendoring::GitEnvironment.clean, chdir: @root)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
require_relative "commands"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Release
|
|
6
|
+
class Runner
|
|
7
|
+
# The npm step: publishes `@hecks/client` from `packages/hecks-client`
|
|
8
|
+
# with the token 1Password holds.
|
|
9
|
+
#
|
|
10
|
+
# `op run` resolves `release/npm_publish.env` into the environment of the
|
|
11
|
+
# one `npm publish` process. npm reads its token from a config file, so a
|
|
12
|
+
# throwaway user config holds a placeholder that npm expands from that
|
|
13
|
+
# environment; the file never contains the token and is deleted afterwards.
|
|
14
|
+
# The package's `prepack` builds `dist/`.
|
|
15
|
+
#
|
|
16
|
+
# The token is a granular one that bypasses two-factor authentication, because
|
|
17
|
+
# the account's second factor is a passkey. As a fallback the publish runs with
|
|
18
|
+
# `--auth-type=web` and its output is not captured, so if npm does ask for an
|
|
19
|
+
# approval it prints the link straight to the terminal and waits for it.
|
|
20
|
+
class NpmPublisher
|
|
21
|
+
PACKAGE_DIR = "packages/hecks-client".freeze
|
|
22
|
+
ENV_FILE = "release/npm_publish.env".freeze
|
|
23
|
+
APPROVAL_NOTICE = "npm will print an approval link; open it and approve with your security key or passkey. " \
|
|
24
|
+
"This step waits for you.".freeze
|
|
25
|
+
USERCONFIG_LINE = "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}".freeze
|
|
26
|
+
|
|
27
|
+
# @param root [String] the repository root
|
|
28
|
+
# @param commands [#run!] runs npm and op
|
|
29
|
+
# @param console [Console] progress output
|
|
30
|
+
def initialize(root:, commands:, console:)
|
|
31
|
+
@root = root
|
|
32
|
+
@commands = commands
|
|
33
|
+
@console = console
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Publishes the package, or with `dry_run` only packs it.
|
|
37
|
+
#
|
|
38
|
+
# @param version [String] the version being released
|
|
39
|
+
# @param dry_run [Boolean] run `npm publish --dry-run`, which needs no credentials
|
|
40
|
+
# @return [void]
|
|
41
|
+
# @raise [CommandFailed] if installing, building or publishing fails
|
|
42
|
+
def publish!(version, dry_run:)
|
|
43
|
+
install_dependencies
|
|
44
|
+
if dry_run
|
|
45
|
+
@console.say("Dry run: npm publish --dry-run for @hecks/client #{version} (nothing is published)...")
|
|
46
|
+
@commands.run!("npm", "publish", "--dry-run", "--access", "public", chdir: package_dir)
|
|
47
|
+
else
|
|
48
|
+
@console.say("Publishing @hecks/client #{version} to npm (1Password will prompt for Touch ID)...")
|
|
49
|
+
@console.say(APPROVAL_NOTICE)
|
|
50
|
+
publish_with_token
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def package_dir
|
|
57
|
+
File.join(@root, PACKAGE_DIR)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def install_dependencies
|
|
61
|
+
return if Dir.exist?(File.join(package_dir, "node_modules"))
|
|
62
|
+
|
|
63
|
+
@console.say("Installing the client's dependencies (npm ci)...")
|
|
64
|
+
@commands.run!("npm", "ci", chdir: package_dir)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def publish_with_token
|
|
68
|
+
with_userconfig do |userconfig|
|
|
69
|
+
@commands.run!(
|
|
70
|
+
"op", "run", "--env-file=#{File.join(@root, ENV_FILE)}", "--",
|
|
71
|
+
"npm", "publish", "--access", "public", "--auth-type=web", "--userconfig", userconfig,
|
|
72
|
+
chdir: package_dir
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def with_userconfig
|
|
78
|
+
file = Tempfile.new(["hecks-npmrc", ".npmrc"])
|
|
79
|
+
file.chmod(0o600)
|
|
80
|
+
file.write("#{USERCONFIG_LINE}\n")
|
|
81
|
+
file.flush
|
|
82
|
+
yield file.path
|
|
83
|
+
ensure
|
|
84
|
+
file&.close!
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "commands"
|
|
3
|
+
require_relative "git"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Release
|
|
7
|
+
class Runner
|
|
8
|
+
# The checks that run before a release does anything: the tools are
|
|
9
|
+
# installed, the checkout is a clean, current `main`, the gem and the
|
|
10
|
+
# JavaScript client agree on a version, and the changelog names it.
|
|
11
|
+
#
|
|
12
|
+
# Every refusal says what to do about it.
|
|
13
|
+
class Preflight
|
|
14
|
+
# What the checks learned about the release.
|
|
15
|
+
#
|
|
16
|
+
# @!attribute [r] version
|
|
17
|
+
# @return [String] the version `lib/hecks/version.rb` declares
|
|
18
|
+
# @!attribute [r] sha
|
|
19
|
+
# @return [String] the commit `HEAD` points at, which is also `origin/main`
|
|
20
|
+
Facts = Struct.new(:version, :sha, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
INSTALL_HINTS = {
|
|
23
|
+
"git" => "install git",
|
|
24
|
+
"gem" => "install Ruby, which ships gem",
|
|
25
|
+
"npm" => "install Node.js: brew install node",
|
|
26
|
+
"curl" => "install curl",
|
|
27
|
+
"op" => "install the 1Password CLI: brew install 1password-cli"
|
|
28
|
+
}.freeze
|
|
29
|
+
VERSION_LINE = /^\s*VERSION\s*=\s*"(?<version>[^"]+)"/
|
|
30
|
+
CLIENT_PACKAGE = "packages/hecks-client/package.json".freeze
|
|
31
|
+
|
|
32
|
+
# @param root [String] the repository root
|
|
33
|
+
# @param commands [#capture] runs the tool probes
|
|
34
|
+
# @param git [Git] the repository the release is cut from
|
|
35
|
+
# @param tools [Array<String>] the executables the release needs on PATH
|
|
36
|
+
def initialize(root:, commands:, git:, tools:)
|
|
37
|
+
@root = root
|
|
38
|
+
@commands = commands
|
|
39
|
+
@git = git
|
|
40
|
+
@tools = tools
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Runs every check.
|
|
44
|
+
#
|
|
45
|
+
# @return [Facts] the version and commit the release is cut from
|
|
46
|
+
# @raise [Refusal] on the first check that fails, naming the fix
|
|
47
|
+
def check!
|
|
48
|
+
check_tools
|
|
49
|
+
check_branch
|
|
50
|
+
check_current
|
|
51
|
+
check_clean
|
|
52
|
+
version = declared_version
|
|
53
|
+
check_client_version(version)
|
|
54
|
+
check_changelog(version)
|
|
55
|
+
Facts.new(version: version, sha: git("rev-parse", "HEAD").strip)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def check_tools
|
|
61
|
+
@tools.each do |tool|
|
|
62
|
+
next if @commands.capture(tool, "--version").success?
|
|
63
|
+
|
|
64
|
+
raise Refusal, "#{tool} not found on PATH; #{INSTALL_HINTS.fetch(tool, "install #{tool}")}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_branch
|
|
69
|
+
branch = git("rev-parse", "--abbrev-ref", "HEAD").strip
|
|
70
|
+
return if branch == "main"
|
|
71
|
+
|
|
72
|
+
raise Refusal, "on branch #{branch}, not main; release from main: git checkout main"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_current
|
|
76
|
+
fetch = @git.capture("fetch", "origin")
|
|
77
|
+
raise Refusal, "git fetch origin failed (#{fetch.stderr.strip}); check the network and the remote" unless fetch.success?
|
|
78
|
+
|
|
79
|
+
head = git("rev-parse", "HEAD").strip
|
|
80
|
+
upstream = git("rev-parse", "origin/main").strip
|
|
81
|
+
return if head == upstream
|
|
82
|
+
|
|
83
|
+
raise Refusal, "main (#{head[0, 7]}) is not origin/main (#{upstream[0, 7]}); " \
|
|
84
|
+
"merge the release PR, then git pull --ff-only"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_clean
|
|
88
|
+
return if git("status", "--porcelain").strip.empty?
|
|
89
|
+
|
|
90
|
+
raise Refusal, "the working tree has uncommitted changes; commit or discard them before releasing"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def declared_version
|
|
94
|
+
match = File.read(File.join(@root, "lib/hecks/version.rb")).match(VERSION_LINE)
|
|
95
|
+
raise Refusal, "lib/hecks/version.rb declares no VERSION; restore it" unless match
|
|
96
|
+
|
|
97
|
+
match[:version]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def check_client_version(version)
|
|
101
|
+
client = JSON.parse(File.read(File.join(@root, CLIENT_PACKAGE))).fetch("version")
|
|
102
|
+
return if client == version
|
|
103
|
+
|
|
104
|
+
raise Refusal, "packages/hecks-client is at #{client} but Hecks::VERSION is #{version}; bump the package first."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def check_changelog(version)
|
|
108
|
+
return if File.read(File.join(@root, "CHANGELOG.md")).match?(/^## \[#{Regexp.escape(version)}\]/)
|
|
109
|
+
|
|
110
|
+
raise Refusal, "CHANGELOG.md has no `## [#{version}]` heading; add the release entry in the release PR"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def git(*)
|
|
114
|
+
@git.read(*)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "commands"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Release
|
|
6
|
+
class Runner
|
|
7
|
+
# Asks the registries whether a version is already out, so a re-run of the
|
|
8
|
+
# release skips what an earlier run finished.
|
|
9
|
+
#
|
|
10
|
+
# Neither question needs credentials.
|
|
11
|
+
class Published
|
|
12
|
+
GEM_VERSIONS_URL = "https://rubygems.org/api/v1/versions/hecks.json".freeze
|
|
13
|
+
NPM_PACKAGE = "@hecks/client".freeze
|
|
14
|
+
|
|
15
|
+
# @param root [String] the repository root
|
|
16
|
+
# @param commands [#capture] runs curl and npm
|
|
17
|
+
def initialize(root:, commands:)
|
|
18
|
+
@root = root
|
|
19
|
+
@commands = commands
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Says whether RubyGems lists the hecks gem at a version.
|
|
23
|
+
#
|
|
24
|
+
# @param version [String] the version to look for
|
|
25
|
+
# @return [Boolean] true when it is published
|
|
26
|
+
# @raise [Refusal] if RubyGems cannot be reached or answers with something unreadable
|
|
27
|
+
def gem?(version)
|
|
28
|
+
result = @commands.capture("curl", "-fsS", GEM_VERSIONS_URL, chdir: @root)
|
|
29
|
+
unless result.success?
|
|
30
|
+
raise Refusal, "could not list published hecks versions (#{result.stderr.strip}); " \
|
|
31
|
+
"check the network and re-run"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
JSON.parse(result.stdout).any? { |entry| entry["number"] == version }
|
|
35
|
+
rescue JSON::ParserError
|
|
36
|
+
raise Refusal, "RubyGems answered with something other than a version list; re-run in a minute"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Says whether npm lists @hecks/client at a version.
|
|
40
|
+
#
|
|
41
|
+
# @param version [String] the version to look for
|
|
42
|
+
# @return [Boolean] true when it is published
|
|
43
|
+
# @raise [Refusal] if npm fails for any reason other than the version not existing
|
|
44
|
+
def npm?(version)
|
|
45
|
+
result = @commands.capture("npm", "view", "#{NPM_PACKAGE}@#{version}", "version", chdir: @root)
|
|
46
|
+
return result.stdout.strip == version if result.success?
|
|
47
|
+
return false if result.stderr.include?("E404")
|
|
48
|
+
|
|
49
|
+
raise Refusal, "could not check npm for #{NPM_PACKAGE}@#{version} (#{result.stderr.strip.lines.first}); " \
|
|
50
|
+
"check the network and re-run"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|