ruby-gem-release 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +23 -18
  4. data/lib/ruby-gem-release/changelog/base.rb +24 -0
  5. data/lib/ruby-gem-release/changelog/generated.rb +48 -0
  6. data/lib/ruby-gem-release/changelog/intridea.rb +16 -2
  7. data/lib/ruby-gem-release/changelog/keep_a_changelog.rb +14 -3
  8. data/lib/ruby-gem-release/changelog.rb +1 -0
  9. data/lib/ruby-gem-release/destination/ruby_gems.rb +69 -0
  10. data/lib/ruby-gem-release/destination.rb +19 -0
  11. data/lib/ruby-gem-release/execution_context/github/run.rb +31 -0
  12. data/lib/ruby-gem-release/execution_context/github/step.rb +28 -0
  13. data/lib/ruby-gem-release/execution_context/github.rb +33 -0
  14. data/lib/ruby-gem-release/execution_context/step.rb +16 -0
  15. data/lib/ruby-gem-release/execution_context.rb +32 -0
  16. data/lib/ruby-gem-release/execution_provider/github.rb +81 -0
  17. data/lib/ruby-gem-release/execution_provider.rb +65 -0
  18. data/lib/ruby-gem-release/github_api.rb +35 -0
  19. data/lib/ruby-gem-release/hosting/github_release/existing.rb +64 -0
  20. data/lib/ruby-gem-release/hosting/github_release.rb +52 -6
  21. data/lib/ruby-gem-release/package/ruby_gem.rb +22 -37
  22. data/lib/ruby-gem-release/release.rb +15 -9
  23. data/lib/ruby-gem-release/release_failure.rb +36 -0
  24. data/lib/ruby-gem-release/semantic_release/conventional_commits.rb +102 -0
  25. data/lib/ruby-gem-release/semantic_release/decision.rb +111 -0
  26. data/lib/ruby-gem-release/semantic_release/git_history.rb +43 -0
  27. data/lib/ruby-gem-release/semantic_release/release_notes.rb +40 -0
  28. data/lib/ruby-gem-release/semantic_release/version_selector.rb +48 -0
  29. data/lib/ruby-gem-release/semantic_release.rb +28 -0
  30. data/lib/ruby-gem-release/source_control/git/preparation.rb +1 -1
  31. data/lib/ruby-gem-release/source_control/git.rb +117 -21
  32. data/lib/ruby-gem-release/tasks.rb +190 -32
  33. data/lib/ruby-gem-release/version/ruby_constant.rb +7 -3
  34. data/lib/ruby-gem-release/version.rb +1 -1
  35. data/lib/ruby-gem-release/workflow/environment.rb +110 -0
  36. data/lib/ruby-gem-release/workflow/failure.rb +47 -0
  37. data/lib/ruby-gem-release/workflow/ruby_gem.rb +57 -13
  38. data/lib/ruby-gem-release/workflow/ruby_gems_credentials.rb +13 -26
  39. data/lib/ruby-gem-release/workflow/semantic_ruby_gem.rb +64 -0
  40. data/lib/ruby-gem-release/workflow/state.rb +94 -0
  41. metadata +25 -3
  42. data/lib/ruby-gem-release/workflow/context.rb +0 -96
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../package/ruby_gem'
4
+ require_relative '../destination/ruby_gems'
5
+ require_relative '../source_control/git'
6
+ require_relative '../release'
7
+ require_relative '../version/ruby_constant'
8
+ require_relative 'state'
9
+ require_relative '../execution_provider'
10
+
11
+ module RubyGemRelease
12
+ module Workflow
13
+ class Environment
14
+ def initialize(provider: ExecutionProvider.current)
15
+ @provider = provider
16
+ end
17
+
18
+ def configure!
19
+ names = %w[INPUT_VERSION INPUT_GEMSPEC_PATH DEFAULT_BRANCH GIT_USER_NAME GIT_USER_EMAIL RUBYGEMS_OWNER_ID]
20
+ timezone = @provider.fetch('TZ', 'UTC')
21
+ validate_timezone(timezone)
22
+ @provider.export(names.to_h { |name| [name, @provider.fetch(name)] }.merge('TZ' => timezone))
23
+ end
24
+
25
+ def capture!(package: nil)
26
+ check_branch
27
+ validate_timezone(@provider.fetch('TZ', 'UTC'))
28
+ git = configured_git
29
+ git.check_clean!
30
+ package ||= Package::RubyGem.new(gemspec: @provider.fetch('INPUT_GEMSPEC_PATH', @provider.fetch('GEMSPEC_PATH', '')) || '', directory: @provider.directory)
31
+ release = initial_release(package)
32
+ git.check_current!(allow_ancestor: true)
33
+ @provider.export(captured_values(package, git, release))
34
+ end
35
+
36
+ def resolve!(package: nil, destination: Destination::RubyGems.new, analysis: State.new(path: @provider.fetch('RELEASE_STATE_FILE', nil)).analysis)
37
+ package ||= Package::RubyGem.new(gemspec: @provider.fetch('GEMSPEC_PATH', @provider.fetch('INPUT_GEMSPEC_PATH', '')) || '', directory: @provider.directory)
38
+ authentication = authentication_for(destination)
39
+ version = prepared_version(package, analysis)
40
+ git = SourceControl::Git.new(directory: @provider.directory)
41
+ git.stage_changes!
42
+ @provider.export('RELEASE_VERSION' => version, 'RELEASE_AUTH_MODE' => authentication)
43
+ end
44
+
45
+ private
46
+
47
+ def authentication_for(destination)
48
+ authentication = destination.authentication
49
+ raise ArgumentError, 'unsupported destination authentication' unless [nil, 'rubygems'].include?(authentication)
50
+
51
+ authentication.to_s
52
+ end
53
+
54
+ def captured_values(package, git, release)
55
+ commit = git.head
56
+ state = State.create!(directory: @provider.directory) if @provider.directory
57
+ { 'TZ' => @provider.fetch('TZ', 'UTC'), 'GEMSPEC_PATH' => package.respond_to?(:gemspec) ? package.gemspec.to_s : '', 'GEMSPEC_VERSION' => release.version,
58
+ 'RELEASE_DATA_DIR' => @provider.directory.to_s,
59
+ 'RELEASE_DATE' => release.date.iso8601, 'SOURCE_COMMIT' => commit, 'RELEASE_STATE_FILE' => state.to_s,
60
+ 'SOURCE_DATE_EPOCH' => source_date_epoch(git, commit) }
61
+ end
62
+
63
+ def source_date_epoch(git, commit)
64
+ value = @provider.fetch('SOURCE_DATE_EPOCH', '').strip
65
+ value.empty? ? git.timestamp(commit) : value
66
+ end
67
+
68
+ def prepared_version(package, analysis)
69
+ version = package.version
70
+ validate_version(version)
71
+ analysis&.check!(head: @provider.fetch('SOURCE_COMMIT'), explicit_version: @provider.fetch('INPUT_VERSION', ''), version: version)
72
+ selected = analysis ? analysis.version : @provider.fetch('INPUT_VERSION', '')
73
+ raise "Prepared version #{version} does not match selected version #{selected}" unless selected.empty? || version == selected
74
+
75
+ version
76
+ end
77
+
78
+ def initial_release(package)
79
+ version = package.version
80
+ validate_version(version)
81
+ validate_version(@provider.fetch('INPUT_VERSION')) unless @provider.fetch('INPUT_VERSION', '').empty?
82
+ Release.new(version: version, gemspec_version: version, date: release_date)
83
+ end
84
+
85
+ def release_date
86
+ (@provider.fetch('TZ', nil) ? Time.now.getlocal : Time.now.utc).to_date
87
+ end
88
+
89
+ def validate_timezone(timezone)
90
+ valid = %r{\A[A-Za-z0-9_+-]+(?:/[A-Za-z0-9_+-]+)*\z}.match?(timezone) &&
91
+ File.file?(File.join('/usr/share/zoneinfo', timezone))
92
+ raise ArgumentError, 'timezone must identify an installed timezone, such as UTC or America/New_York' unless valid
93
+ end
94
+
95
+ def configured_git
96
+ SourceControl::Git.new(directory: @provider.directory).tap { |git| git.configure!(**@provider.git_identity) }
97
+ end
98
+
99
+ def check_branch
100
+ return if @provider.ref == "refs/heads/#{@provider.fetch('DEFAULT_BRANCH')}"
101
+
102
+ raise 'Releases must run from the default branch'
103
+ end
104
+
105
+ def validate_version(version)
106
+ raise ArgumentError, 'Version must be MAJOR.MINOR.PATCH without leading zeros' unless Version::RubyConstant::PATTERN.match?(version)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../release_failure'
5
+ require_relative '../execution_provider'
6
+
7
+ module RubyGemRelease
8
+ module Workflow
9
+ class Failure
10
+ class << self
11
+ def phase!(provider: ExecutionProvider.current)
12
+ failed = provider.failed_steps
13
+ phase = failed.find { |step| error_for(step, provider: provider) } || failed.first || 'unknown'
14
+ provider.export('RELEASE_FAILED_STEP_ID' => phase)
15
+ end
16
+
17
+ def capture!(phase, error, provider: ExecutionProvider.current)
18
+ return unless provider.directory
19
+
20
+ captured_error = ReleaseFailure::CapturedError.capture(error)
21
+ File.open(failure_file(provider), 'w', 0o600) do |file|
22
+ file.write(JSON.generate('phase' => phase, 'error' => captured_error.to_h))
23
+ end
24
+ rescue StandardError => e
25
+ warn "Unable to capture release error details: #{e.message}"
26
+ end
27
+
28
+ def error_for(phase, provider: ExecutionProvider.current)
29
+ return unless provider.directory
30
+
31
+ failure = JSON.parse(File.read(failure_file(provider)))
32
+ return unless failure.fetch('phase') == phase
33
+
34
+ ReleaseFailure::CapturedError.from_h(failure.fetch('error'))
35
+ rescue Errno::ENOENT
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def failure_file(provider)
42
+ File.join(provider.directory, 'ruby-gem-release-failure.json')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -2,33 +2,68 @@
2
2
 
3
3
  require_relative '../tasks'
4
4
  require_relative '../package/ruby_gem'
5
+ require_relative '../destination/ruby_gems'
5
6
  require_relative '../source_control/git'
6
7
  require_relative '../hosting/github_release'
7
8
 
8
9
  module RubyGemRelease
9
10
  module Workflow
10
11
  class RubyGem
11
- def initialize(version:, changelog:, package: Package::RubyGem.new, git: SourceControl::Git.new, hosting: Hosting::GitHubRelease.new)
12
+ attr_reader :version, :changelog, :package, :destination, :git, :hosting, :provider
13
+
14
+ def initialize(version:, changelog:, provider: ExecutionProvider.current,
15
+ package: Package::RubyGem.new(gemspec: provider.fetch('GEMSPEC_PATH', provider.fetch('INPUT_GEMSPEC_PATH', '')) || '', directory: provider.directory),
16
+ destination: Destination::RubyGems.new,
17
+ git: SourceControl::Git.new(directory: provider.directory), hosting: Hosting::GitHubRelease.new,
18
+ validate: nil, success: nil, failure: nil)
12
19
  @version = version
13
20
  @changelog = changelog
14
21
  @package = package
22
+ @destination = destination
15
23
  @git = git
16
24
  @hosting = hosting
25
+ @provider = provider
26
+ @validate = validate
27
+ @success = success
28
+ @failure = failure
17
29
  end
18
30
 
19
31
  def install_tasks
20
- Tasks.install do |tasks|
21
- tasks.before_release { |release| prepare_release(release) }
22
- tasks.publish { |release| publish(release) }
23
- tasks.after_release { |release| prepare_next(release) }
32
+ Tasks.install(workflow: self, package: @package, destination: @destination, provider: @provider) do |tasks|
33
+ install_callbacks(tasks)
24
34
  end
25
35
  end
26
36
 
37
+ def checkpoint!(context)
38
+ prepare_publication(context.release)
39
+ end
40
+
41
+ def published?(release)
42
+ @package.respond_to?(:built?) && @package.built?(release) && @destination.published?(release, package: @package)
43
+ end
44
+
27
45
  private
28
46
 
47
+ def install_callbacks(tasks)
48
+ tasks.validate { |ctx| validate_release(ctx) }
49
+ tasks.prepare { |ctx| prepare_release(ctx.release) }
50
+ tasks.publish { |ctx| publish(ctx.release) }
51
+ tasks.bump { |ctx| prepare_next(ctx) }
52
+ tasks.success(&@success) if @success
53
+ tasks.failure(&@failure) if @failure
54
+ end
55
+
56
+ def validate_release(ctx)
57
+ return @validate.call(ctx) if @validate
58
+
59
+ return if @changelog.nil? || @changelog.released?(ctx.release.version) || @changelog.pending?
60
+
61
+ raise ArgumentError, 'Nothing to release: no pending changelog notes'
62
+ end
63
+
29
64
  def prepare_release(release)
30
65
  @git.stage_changes! do
31
- select_version!(release.version) unless ENV.fetch('INPUT_VERSION', '').empty?
66
+ select_version!(release.version) unless @provider.fetch('INPUT_VERSION', '').empty?
32
67
  @changelog&.release!(release.with_version(@version.read))
33
68
  end
34
69
  end
@@ -39,24 +74,33 @@ module RubyGemRelease
39
74
  @version.set!(version)
40
75
  end
41
76
 
42
- def publish(release)
43
- @package.check_version!(release.version)
77
+ def prepare_publication(release)
44
78
  @git.commit!(release)
45
79
  @package.build!(release)
46
80
  @git.tag!(release)
47
- @package.publish!(release)
81
+ end
82
+
83
+ def publish(release)
84
+ prepare_publication(release)
85
+ @destination.publish!(release, package: @package)
48
86
  @git.push!(release)
49
87
  @hosting&.publish!(release, package: @package)
50
88
  end
51
89
 
52
- def prepare_next(release)
90
+ def prepare_next(ctx)
91
+ next_release = stage_next!(ctx.release)
92
+ @git.commit_next!(next_release)
93
+ @git.push!
94
+ ctx.record_next_version!(next_release.next_version)
95
+ end
96
+
97
+ def stage_next!(release)
53
98
  next_release = nil
54
99
  @git.stage_changes! do
55
- next_release = release.with_next_version(@version.increment!)
100
+ next_release = release.with_next_version(@version.increment!(from: release.version))
56
101
  @changelog&.prepare_next!(next_release)
57
102
  end
58
- @git.commit_next!(next_release)
59
- @git.push!
103
+ next_release
60
104
  end
61
105
  end
62
106
  end
@@ -3,45 +3,36 @@
3
3
  require 'net/http'
4
4
  require 'json'
5
5
  require 'uri'
6
+ require_relative '../execution_provider'
6
7
 
7
8
  module RubyGemRelease
8
9
  module Workflow
9
10
  class RubyGemsCredentials
10
- def initialize(owner:)
11
- raise ArgumentError, 'RubyGems owner must be a positive numeric account ID' unless /\A[1-9]\d*\z/.match?(owner.to_s)
12
-
11
+ def initialize(owner: nil, provider: ExecutionProvider.current)
13
12
  @owner = owner.to_s
13
+ raise ArgumentError, 'RubyGems owner must be a positive numeric account ID' unless @owner.empty? || /\A[1-9]\d*\z/.match?(@owner)
14
+
15
+ @provider = provider
14
16
  end
15
17
 
16
18
  def configure!
17
- token = oidc_token
18
- mask(token)
19
+ token = @provider.oidc_token(audience: 'rubygems.org')
20
+ @provider.mask(token)
19
21
  key = exchange_token(token)
20
- mask(key)
21
- File.open(ENV.fetch('GITHUB_ENV'), 'a') do |file|
22
- %w[RUBYGEMS_API_KEY BUNDLE_GEM__PUSH_KEY GEM_HOST_API_KEY].each { |name| file.puts "#{name}=#{key}" }
23
- end
22
+ @provider.mask(key)
23
+ @provider.export(%w[RUBYGEMS_API_KEY BUNDLE_GEM__PUSH_KEY GEM_HOST_API_KEY].to_h { |name| [name, key] })
24
24
  end
25
25
 
26
26
  private
27
27
 
28
- def oidc_token
29
- uri = URI(ENV.fetch('ACTIONS_ID_TOKEN_REQUEST_URL'))
30
- raise ArgumentError, 'OIDC request URL must use HTTPS' unless uri.is_a?(URI::HTTPS)
31
-
32
- query = URI.decode_www_form(uri.query || '').reject { |name, _value| name == 'audience' } # rubocop:disable Style/HashExcept -- Query pairs may contain duplicate keys.
33
- uri.query = URI.encode_www_form(query + [['audience', 'rubygems.org']])
34
- request = Net::HTTP::Get.new(uri)
35
- request['Authorization'] = "Bearer #{ENV.fetch('ACTIONS_ID_TOKEN_REQUEST_TOKEN')}"
36
- credential(response(uri, request), 'value')
37
- end
38
-
39
28
  def exchange_token(token)
40
29
  uri = URI('https://rubygems.org/api/v1/oidc/trusted_publisher/exchange_token')
41
30
  request = Net::HTTP::Post.new(uri)
42
31
  request['Content-Type'] = 'application/json'
43
32
  request['Accept'] = 'application/json'
44
- request.body = JSON.generate(jwt: token, owner: @owner)
33
+ body = { jwt: token }
34
+ body[:owner] = @owner unless @owner.empty?
35
+ request.body = JSON.generate(body)
45
36
  credential(response(uri, request), 'rubygems_api_key')
46
37
  end
47
38
 
@@ -49,7 +40,7 @@ module RubyGemRelease
49
40
  result = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30, max_retries: 0) do |http|
50
41
  http.request(request)
51
42
  end
52
- raise "Credential request failed (HTTP #{result.code}); verify owner ID and pending trusted publisher" unless result.is_a?(Net::HTTPSuccess)
43
+ raise "Credential request failed (HTTP #{result.code}); verify trusted publisher configuration and owner ID if supplied" unless result.is_a?(Net::HTTPSuccess)
53
44
 
54
45
  JSON.parse(result.body)
55
46
  rescue JSON::ParserError
@@ -62,10 +53,6 @@ module RubyGemRelease
62
53
 
63
54
  value
64
55
  end
65
-
66
- def mask(value)
67
- puts "::add-mask::#{value.gsub('%', '%25')}"
68
- end
69
56
  end
70
57
  end
71
58
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ruby_gem'
4
+ require_relative '../semantic_release'
5
+ require_relative '../changelog/generated'
6
+
7
+ module RubyGemRelease
8
+ module Workflow
9
+ class SemanticRubyGem < RubyGem
10
+ attr_reader :semantic_release
11
+
12
+ def initialize(semantic_release: SemanticRelease.new, changelog: Changelog::Generated.new, bump: false, **)
13
+ raise ArgumentError, 'bump must be true or false' unless [true, false].include?(bump)
14
+
15
+ @semantic_release = semantic_release
16
+ @bump = bump
17
+ super(changelog: changelog, **)
18
+ end
19
+
20
+ private
21
+
22
+ def install_callbacks(tasks)
23
+ tasks.validate { |ctx| validate_release(ctx) }
24
+ tasks.prepare { |ctx| prepare_semantic_release(ctx) }
25
+ install_result_callbacks(tasks)
26
+ tasks.failure(&@failure) if @failure
27
+ end
28
+
29
+ def install_result_callbacks(tasks)
30
+ tasks.publish { |ctx| with_decision(ctx) { publish(ctx.release) } }
31
+ tasks.bump { |ctx| with_decision(ctx) { prepare_next(ctx) if @bump } }
32
+ tasks.success { |ctx| with_decision(ctx) { @success&.call(ctx) } }
33
+ end
34
+
35
+ def with_decision(ctx)
36
+ checked_decision(ctx)
37
+ yield
38
+ end
39
+
40
+ def validate_release(ctx)
41
+ decision = ctx.analysis || @semantic_release.analyze(head: @provider.fetch('SOURCE_COMMIT'), explicit_version: @provider.fetch('INPUT_VERSION', ''))
42
+ ctx.record_analysis!(decision) unless ctx.analysis
43
+ raise ArgumentError, 'Nothing to release: no release-eligible commits' unless decision.release?
44
+
45
+ @validate&.call(ctx)
46
+ end
47
+
48
+ def checked_decision(ctx)
49
+ raise ArgumentError, 'semantic release requires a persisted release decision' unless ctx.analysis&.release?
50
+
51
+ ctx.analysis.check!(head: @provider.fetch('SOURCE_COMMIT'), explicit_version: @provider.fetch('INPUT_VERSION', ''), version: ctx.release.version)
52
+ ctx.analysis
53
+ end
54
+
55
+ def prepare_semantic_release(ctx)
56
+ checked_decision(ctx)
57
+ @git.stage_changes! do
58
+ select_version!(ctx.release.version)
59
+ @changelog&.release!(ctx.release)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'tempfile'
5
+ require_relative '../version/ruby_constant'
6
+ require_relative '../semantic_release/decision'
7
+
8
+ module RubyGemRelease
9
+ module Workflow
10
+ class State
11
+ def self.create!(directory:)
12
+ file = Tempfile.create(['ruby-gem-release-state-', '.json'], directory)
13
+ file.write(JSON.generate('next_version' => nil, 'analysis' => nil))
14
+ file.path
15
+ ensure
16
+ file&.close
17
+ end
18
+
19
+ def initialize(path: nil)
20
+ @path = path unless path.to_s.empty?
21
+ @next_version = nil
22
+ @analysis = nil
23
+ end
24
+
25
+ def next_version
26
+ snapshot.fetch(:next_version)
27
+ end
28
+
29
+ def analysis
30
+ snapshot.fetch(:analysis)
31
+ end
32
+
33
+ def snapshot
34
+ return { next_version: @next_version, analysis: @analysis }.freeze unless @path
35
+
36
+ values, analysis = read_state
37
+ { next_version: values.fetch('next_version')&.freeze, analysis: analysis }.freeze
38
+ end
39
+
40
+ def record_analysis!(decision)
41
+ raise ArgumentError, 'analysis must be a semantic release decision' unless decision.is_a?(SemanticRelease::Decision)
42
+ raise ArgumentError, 'semantic analysis is already recorded' if analysis
43
+
44
+ if @path
45
+ persist(attributes.merge('analysis' => decision.to_h))
46
+ else
47
+ @analysis = decision
48
+ end
49
+ decision
50
+ end
51
+
52
+ def record_next_version!(version)
53
+ validate_version(version)
54
+ if @path
55
+ persist(attributes.merge('next_version' => version))
56
+ else
57
+ @next_version = version.dup.freeze
58
+ end
59
+ version
60
+ end
61
+
62
+ private
63
+
64
+ def attributes
65
+ read_state.first
66
+ end
67
+
68
+ def read_state
69
+ values = JSON.parse(File.read(@path))
70
+ valid = values.is_a?(Hash) && values.keys.sort == %w[analysis next_version]
71
+ raise ArgumentError, 'unsupported release state' unless valid
72
+
73
+ validate_version(values['next_version']) unless values['next_version'].nil?
74
+ raw_analysis = values['analysis']
75
+ analysis = SemanticRelease::Decision.from_h(raw_analysis) unless raw_analysis.nil?
76
+ [values, analysis]
77
+ end
78
+
79
+ def persist(values)
80
+ Tempfile.create(['.release-state-', '.json'], File.dirname(@path)) do |file|
81
+ file.write(JSON.generate(values))
82
+ file.close
83
+ File.rename(file.path, @path)
84
+ end
85
+ end
86
+
87
+ def validate_version(version)
88
+ return if version.is_a?(String) && Version::RubyConstant::PATTERN.match?(version)
89
+
90
+ raise ArgumentError, 'next version must be MAJOR.MINOR.PATCH'
91
+ end
92
+ end
93
+ end
94
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-gem-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Doubrovkine
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-11-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: commonmarker
@@ -50,19 +50,41 @@ files:
50
50
  - lib/ruby-gem-release.rb
51
51
  - lib/ruby-gem-release/changelog.rb
52
52
  - lib/ruby-gem-release/changelog/base.rb
53
+ - lib/ruby-gem-release/changelog/generated.rb
53
54
  - lib/ruby-gem-release/changelog/intridea.rb
54
55
  - lib/ruby-gem-release/changelog/keep_a_changelog.rb
56
+ - lib/ruby-gem-release/destination.rb
57
+ - lib/ruby-gem-release/destination/ruby_gems.rb
58
+ - lib/ruby-gem-release/execution_context.rb
59
+ - lib/ruby-gem-release/execution_context/github.rb
60
+ - lib/ruby-gem-release/execution_context/github/run.rb
61
+ - lib/ruby-gem-release/execution_context/github/step.rb
62
+ - lib/ruby-gem-release/execution_context/step.rb
63
+ - lib/ruby-gem-release/execution_provider.rb
64
+ - lib/ruby-gem-release/execution_provider/github.rb
65
+ - lib/ruby-gem-release/github_api.rb
55
66
  - lib/ruby-gem-release/hosting/github_release.rb
67
+ - lib/ruby-gem-release/hosting/github_release/existing.rb
56
68
  - lib/ruby-gem-release/package/ruby_gem.rb
57
69
  - lib/ruby-gem-release/release.rb
70
+ - lib/ruby-gem-release/release_failure.rb
71
+ - lib/ruby-gem-release/semantic_release.rb
72
+ - lib/ruby-gem-release/semantic_release/conventional_commits.rb
73
+ - lib/ruby-gem-release/semantic_release/decision.rb
74
+ - lib/ruby-gem-release/semantic_release/git_history.rb
75
+ - lib/ruby-gem-release/semantic_release/release_notes.rb
76
+ - lib/ruby-gem-release/semantic_release/version_selector.rb
58
77
  - lib/ruby-gem-release/source_control/git.rb
59
78
  - lib/ruby-gem-release/source_control/git/preparation.rb
60
79
  - lib/ruby-gem-release/tasks.rb
61
80
  - lib/ruby-gem-release/version.rb
62
81
  - lib/ruby-gem-release/version/ruby_constant.rb
63
- - lib/ruby-gem-release/workflow/context.rb
82
+ - lib/ruby-gem-release/workflow/environment.rb
83
+ - lib/ruby-gem-release/workflow/failure.rb
64
84
  - lib/ruby-gem-release/workflow/ruby_gem.rb
65
85
  - lib/ruby-gem-release/workflow/ruby_gems_credentials.rb
86
+ - lib/ruby-gem-release/workflow/semantic_ruby_gem.rb
87
+ - lib/ruby-gem-release/workflow/state.rb
66
88
  homepage: https://github.com/dblock/ruby-gem-release
67
89
  licenses:
68
90
  - MIT
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../package/ruby_gem'
4
- require_relative '../source_control/git'
5
- require_relative '../release'
6
- require_relative '../version/ruby_constant'
7
-
8
- module RubyGemRelease
9
- module Workflow
10
- class Context
11
- def configure!
12
- names = %w[INPUT_VERSION INPUT_GEMSPEC DEFAULT_BRANCH GIT_USER_NAME GIT_USER_EMAIL RUBYGEMS_OWNER]
13
- timezone = ENV.fetch('TZ', 'UTC')
14
- validate_timezone(timezone)
15
- export(names.to_h { |name| [name, ENV.fetch(name)] }.merge('TZ' => timezone))
16
- end
17
-
18
- def capture!
19
- check_branch
20
- validate_timezone(ENV.fetch('TZ', 'UTC'))
21
- git = configured_git
22
- git.check_clean!
23
- package = Package::RubyGem.new(gemspec: ENV.fetch('INPUT_GEMSPEC', nil))
24
- release = initial_release(package)
25
- git.check_current!(release)
26
- export('TZ' => ENV.fetch('TZ', 'UTC'), 'GEMSPEC' => package.gemspec, 'RELEASE_PREVIOUS_VERSION' => release.version,
27
- 'RELEASE_DATE' => release.date.iso8601, 'RELEASE_INITIAL_COMMIT' => git.head)
28
- end
29
-
30
- def resolve!
31
- package = Package::RubyGem.new
32
- version = prepared_version(package)
33
- git = SourceControl::Git.new
34
- git.check_tag!(version)
35
- package.check_version!(version)
36
- git.stage_changes!
37
- export('RELEASE_VERSION' => version)
38
- end
39
-
40
- private
41
-
42
- def prepared_version(package)
43
- version = package.version
44
- validate_version(version)
45
- selected = ENV.fetch('INPUT_VERSION', '')
46
- raise "Prepared version #{version} does not match selected version #{selected}" unless selected.empty? || version == selected
47
-
48
- version
49
- end
50
-
51
- def initial_release(package)
52
- version = package.version
53
- validate_version(version)
54
- validate_version(ENV.fetch('INPUT_VERSION')) unless ENV.fetch('INPUT_VERSION', '').empty?
55
- Release.new(version: version, previous_version: version, date: release_date)
56
- end
57
-
58
- def release_date
59
- (ENV.key?('TZ') ? Time.now.getlocal : Time.now.utc).to_date
60
- end
61
-
62
- def validate_timezone(timezone)
63
- valid = %r{\A[A-Za-z0-9_+-]+(?:/[A-Za-z0-9_+-]+)*\z}.match?(timezone) &&
64
- File.file?(File.join('/usr/share/zoneinfo', timezone))
65
- raise ArgumentError, 'timezone must identify an installed timezone, such as UTC or America/New_York' unless valid
66
- end
67
-
68
- def configured_git
69
- SourceControl::Git.new.tap do |git|
70
- git.configure!(name: ENV.fetch('GIT_USER_NAME', 'github-actions[bot]'),
71
- email: ENV.fetch('GIT_USER_EMAIL', 'github-actions[bot]@users.noreply.github.com'))
72
- end
73
- end
74
-
75
- def check_branch
76
- return if ENV.fetch('GITHUB_REF') == "refs/heads/#{ENV.fetch('DEFAULT_BRANCH')}"
77
-
78
- raise 'Releases must run from the default branch'
79
- end
80
-
81
- def validate_version(version)
82
- raise ArgumentError, 'Version must be MAJOR.MINOR.PATCH without leading zeros' unless Version::RubyConstant::PATTERN.match?(version)
83
- end
84
-
85
- def export(values)
86
- raise ArgumentError, 'Workflow values cannot contain line breaks' if values.values.any? { |value| value.match?(/[\r\n]/) }
87
-
88
- File.open(ENV.fetch('GITHUB_ENV'), 'a') do |file|
89
- values.each do |key, value|
90
- file.puts "#{key}=#{value}"
91
- end
92
- end
93
- end
94
- end
95
- end
96
- end