Module: KKGit::Release

Defined in:
lib/kk/git/release.rb

Overview

Semver bump and git tag helpers (maintainer release flow).

Constant Summary collapse

VERSION_PATH =
File.join(__dir__, 'version.rb').freeze

Class Method Summary collapse

Class Method Details

.bump_semver(version, level) ⇒ Object



27
28
29
30
31
32
33
34
# File 'lib/kk/git/release.rb', line 27

def bump_semver(version, level)
  major, minor, patch = version
  case level
  when 'major' then [major + 1, 0, 0]
  when 'minor' then [major, minor + 1, 0]
  else [major, minor, patch + 1]
  end
end

.create_and_push_tag!(tag, remote: nil, repo_dir: '.') ⇒ Object



103
104
105
106
107
108
109
110
111
# File 'lib/kk/git/release.rb', line 103

def create_and_push_tag!(tag, remote: nil, repo_dir: '.')
  remote ||= GitOps.remote
  out, err, ok = GitOps.run_cmd('git', 'tag', '-a', tag, '-m', "Release #{tag}", chdir: repo_dir)
  GitOps.ensure_ok!(ok, 'git tag', stdout: out, stderr: err, repo_dir: repo_dir)

  push_target = GitOps.push_target(remote)
  out, err, ok = GitOps.run_cmd('git', 'push', push_target, tag, chdir: repo_dir)
  GitOps.ensure_ok!(ok, 'git push tag', stdout: out, stderr: err, repo_dir: repo_dir)
end

.current_version_in_fileObject



59
60
61
62
63
# File 'lib/kk/git/release.rb', line 59

def current_version_in_file
  content = File.read(VERSION_PATH, mode: 'r:BOM|UTF-8')
  m = content.match(/VERSION\s*=\s*'([^']*)'/)
  m ? parse_semver(m[1]) : nil
end

.latest_semver_tag(prefix: 'v', repo_dir: '.') ⇒ Object



10
11
12
13
14
15
16
17
18
# File 'lib/kk/git/release.rb', line 10

def latest_semver_tag(prefix: 'v', repo_dir: '.')
  stdout, _stderr, ok = GitOps.run_cmd(
    'git', 'tag', '--list', "#{prefix}[0-9]*.[0-9]*.[0-9]*", '--sort=-v:refname',
    chdir: repo_dir
  )
  return nil unless ok

  stdout.to_s.split("\n").first&.strip
end

.next_tag_and_version(prefix: nil, bump: nil, repo_dir: '.') ⇒ Array(String, String)

Returns tag, version.

Returns:

  • (Array(String, String))

    tag, version



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/kk/git/release.rb', line 78

def next_tag_and_version(prefix: nil, bump: nil, repo_dir: '.')
  prefix ||= ENV.fetch('KK_GIT_TAG_PREFIX', 'v')
  bump ||= ENV.fetch('KK_GIT_BUMP', 'patch')

  latest_tag = latest_semver_tag(prefix: prefix, repo_dir: repo_dir)
  tag_version = latest_tag ? parse_semver(latest_tag.delete_prefix(prefix)) : nil
  file_version = current_version_in_file || parse_semver(KKGit::VERSION)

  # version.rb already ahead of tags and tag not yet created → release that version
  if file_version && tag_version && semver_gt?(file_version, tag_version)
    candidate = "#{prefix}#{semver_to_s(file_version)}"
    return [candidate, semver_to_s(file_version)] unless tag_exists?(candidate, repo_dir: repo_dir)
  end

  base = semver_max(tag_version, file_version) || [0, 1, 0]
  version = base
  50.times do
    version = bump_semver(version, bump)
    candidate = "#{prefix}#{semver_to_s(version)}"
    return [candidate, semver_to_s(version)] unless tag_exists?(candidate, repo_dir: repo_dir)
  end

  raise 'Failed to generate next release tag: too many attempts'
end

.parse_semver(str) ⇒ Object



20
21
22
23
24
25
# File 'lib/kk/git/release.rb', line 20

def parse_semver(str)
  m = str.to_s.match(/\A(\d+)\.(\d+)\.(\d+)\z/)
  return nil unless m

  [m[1].to_i, m[2].to_i, m[3].to_i]
end

.semver_gt?(a, b) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
43
44
45
46
47
48
# File 'lib/kk/git/release.rb', line 40

def semver_gt?(a, b)
  return false if a.nil? || b.nil?

  a.each_with_index do |part, i|
    return true if part > b[i]
    return false if part < b[i]
  end
  false
end

.semver_max(*versions) ⇒ Object



50
51
52
# File 'lib/kk/git/release.rb', line 50

def semver_max(*versions)
  versions.compact.max_by { |v| v }
end

.semver_to_s(version) ⇒ Object



36
37
38
# File 'lib/kk/git/release.rb', line 36

def semver_to_s(version)
  "#{version[0]}.#{version[1]}.#{version[2]}"
end

.tag_exists?(tag, repo_dir: '.') ⇒ Boolean

Returns:

  • (Boolean)


54
55
56
57
# File 'lib/kk/git/release.rb', line 54

def tag_exists?(tag, repo_dir: '.')
  out, _err, ok = GitOps.run_cmd('git', 'tag', '--list', tag, chdir: repo_dir)
  ok && !out.to_s.strip.empty?
end

.update_version!(new_version) ⇒ Boolean

Returns true when file was updated.

Returns:

  • (Boolean)

    true when file was updated



66
67
68
69
70
71
72
73
74
75
# File 'lib/kk/git/release.rb', line 66

def update_version!(new_version)
  content = File.read(VERSION_PATH, mode: 'r:BOM|UTF-8')
  return false if content.match?(/VERSION\s*=\s*'#{Regexp.escape(new_version)}'/)

  replaced = content.sub(/VERSION\s*=\s*'[^']*'/, "VERSION = '#{new_version}'")
  raise "Failed to update version in #{VERSION_PATH}" if replaced == content

  File.write(VERSION_PATH, replaced)
  true
end